[Перевод] Ruby 2.1 в деталях (Часть 1)
Ruby 2.1, последняя значимая версия Ruby (на момент написания поста), была выпущена в Рождество 2013, спустя всего лишь 10 месяцев после выхода 2.0.0. Она вышла с целым рядом изменений и улучшений, и данный пост в деталях описывает эти новшества.Новая политика управления версиями С версией 2.1 Ruby переходит на новую схему изменения версий на базе Semantic Versioning.Версии выходят по схеме MAJOR.MINOR.TEENY, т.е. в версии 2.1.0: 2 — мажорная версия, 1 — минорная, младшая версия — 0. Номер младшей версии берется из патчлевела для минорного бага и уязвимостей безопасности. Номер минорной версии будет использоваться для новых фич, по большей части обратно совместимых, а мажорной — для несовместимых изменений, который не могут быть выпущены под минорной версией.
Это означает, что вместо, к примеру 1.9.3 и 1.9.3-p545 мы получим релизы вида 2.1 и 2.1.1.
Планируется выпускать минорные версии каждые 12 месяцев, т.е. мы можем ожидать Ruby 2.2 к Рождеству 2014.
Обязательные именованные аргументы В именованные аргументы, появившиеся в Ruby 2.0.0 было добавлено небольшое улучшение. Теперь можно не указывать значение по умолчанию для именованных аргументов при определении метода, и если при вызове метода они не будут заданы, будет возбуждена ошибка. # length is required def pad (num, length:, char:»0») num.to_s.rjust (length, char) end
pad (42, length: 6) #=> »000042»
pad (42) #=> #
# returns new String object on each call env.object_id #=> 70329318373020 env.object_id #=> 70329318372900 Это может быть весьма расточительно — сначала создавать некоторое количество объектов, а затем удалять их сборщиком мусора. Чтобы избежать этого, можно напрямую вызвать метод #freeze, который означает, что поиск строки будет проходить в таблице «замороженных» строк, и один и тот же объект будет использоваться каждый раз: def env «development».freeze end
# returns the same String object on each call env.object_id #=> 70365553080120 env.object_id #=> 70365553080120 Строковые литералы, являющиеся ключами хэша, будут обрабатыаться так же без необходимости вызова #freeze. a = {«name» => «Arthur»} b = {«name» => «Ford»}
# same String object used as key in both hashes a.keys.first.object_id #=> 70253124073040 b.keys.first.object_id #=> 70253124073040 В процессе разработки 2.1 эта фича изначально была изменением в синтаксисе, где «string«f обозначало «замороженную» строку. Однако было решено перейти от такого изменения к вызову метода #freeze, что не ломает прямую и обратную совместимость, плюс к тому многие люди недолюбливают лишние изменения в синтаксисе.def возвращает имя метода как объект Symbol Результатом объявления метода теперь является не nil, а объект Symbol, соответствующий имени метода. Каноническим примером использования этого является объявление приватным только одного метода: class Client def initialize (host, port) # … end
private def do_request (method, path, body, **headers) # … end
def get (path, **headers) do_request (: get, path, nil, **headers) end end Это также может использоваться для добавления декораторов к методам, ниже приведен пример использования Module#prepend для оборачивания метода в before/after коллбэки: module Around def around (method) prepend (Module.new do define_method (method) do |*args, &block| send (: «before_#{method}») if respond_to?(: «before_#{method}», true) result = super (*args, &block) send (: «after_#{method}») if respond_to?(: «after_#{method}», true) result end end) method end end
class Example extend Around
around def call puts «call» end
def before_call puts «before» end
def after_call puts «after» end end
Example.new.call выведет before call after Методы define_method и define_singleton_method также были изменены и теперь возвращают объекты Symbol вместо их proc-аргументов.Литералы для рациональных и комплексных чисел В Ruby есть литералы для классов Integer (1) и Float (1.0), теперь к ним добавились литералы для классов Rational (1r) и Complex (1i).Они хорошо работают с механизмом приведения для математических операций в Ruby, так что ⅓ может быть записана в Ruby как 1/3r. 3i представляет комплексное число 0+3i, что означает, что комплексные числа могут быть записаны в стандартной математической нотации, 2+3i в Ruby представляет комплексное число 2+3i!
Array/Enumerable #to_h Множество классов, получивших метод #to_h в Ruby 2.0.0 теперь пополнилось классом Array и любым другим классом, включающим Enumerable. [[: id, 42], [: name, «Arthur»]].to_h #=> {: id=>42, : name=>«Arthur»}
require «set» Set[[: id, 42], [: name, «Arthur»]].to_h #=> {: id=>42, : name=>«Arthur»} Это может быть полезно при использовании методов Hash, возвращающих Array: headers = {«Content-Length» => 42, «Content-Type» => «text/html»} headers.map {|k, v| [k.downcase, v]}.to_h #=> {«content-length» => 42, «content-type» => «text/html»} Разделенное кэширование методов До версии 2.1 Ruby использовал глобальный кэш методов, который инвалидировался для всех классов при добавлении нового метода, подключении модуля, включении модуля в объект и т.д. в любом месте вашего кода. Это делало некоторые классы (такие как OpenStruct) и некоторые приемы (такие как тэггирование исключений) бесполезными ввиду соображений производительности.Теперь это не является проблемой, Ruby 2.1 использует кэширование методов, базирующееся на иерархии классов, инвалидируя кэш только для заданного класса и его подклассов.
В класс RubyVM был добавлен метод, возвращающий некоторую отладочную информацию для кэша методов:
class Foo end
RubyVM.stat #=> {: global_method_state=>133, : global_constant_state=>820, : class_serial=>5689}
# setting constant increments: global_constant_state
Foo: Bar = «bar»
RubyVM.stat (: global_constant_state) #=> 821
# defining instance method increments: class_serial
class Foo def foo end end
RubyVM.stat (: class_serial) #=> 5690
# defining global method increments: global_method_state
def foo end
RubyVM.stat (: global_method_state) #=> 134 Исключения У объектов исключений теперь есть метод #cause, возвращающий исключение, возбудившее данное. Оно устанавливается автоматически, когда вы перехватываете одно исключение и возбуждаете другое. require «socket»
module MyProject Error = Class.new (StandardError) NotFoundError = Class.new (Error) ConnectionError = Class.new (Error)
def self.get (path) response = do_get (path) raise NotFoundError,»#{path} not found» if response.code == »404» response.body rescue Errno: ECONNREFUSED, SocketError => e raise ConnectionError end end
begin
MyProject.get (»/example»)
rescue MyProject: Error => e
e #=> #
Поколенческий сборщик мусора В Ruby 2.1 введен сборщик мусора на основе поколений, который разделяет все объекты на младшее и старшее поколения. При обычном запуске GC будет просматривать только объекты младшего поколения, объекты же старшего поколения будут просматриваться значительно реже. Удаление объектов (sweeping) производится по той же схеме, что и в 1.9.3(lazy sweep). Если объект из младшего поколения «выживает» при запуске GC, он переходит в старшее поколение.Если у вас есть объекты старшего поколения, ссылающиеся на объекты младшего поколения, при обработке только младшего поколения GC может ошибочно посчитать, что никаких ссылок на этот объект нет и удалить его. Для предотвращения этого были введены барьеры записи, добавляющие объекты старшего поколения в особое запоминаемое множество (remember set), когда они при изменении начинают ссылаться на объекты младшего поколения (например old_array.push (young_string)). Это множество затем учитывается при маркировке (marking) младшего поколения.
Большинству поколенческих сборщиков мусора такие барьеры нужны для всех объектов, но для многих сторонних C-расширений для Ruby это невозможно. Поэтому в качестве временного решения было введено, что объекты, для которых не созданы барьеры (т.н. «теневые» объекты) никогда не попадают в старшее поколение. Это решение неидеально в плане полного использования всех возможностей поколенческого сборщика мусора, но зато предоставляет максимальную обратную совместимость.
Теперь фаза маркировки проходит значительно быстрее, но наличие барьеров записи означает дополнительные накладные расходы, поэтому отличия в производительности напрямую зависят от того, что делает ваш код.
Класс GC Метод GC.start может получать два новых параметра, full_mark и immediate_sweep. Оба по умочанию true.Если full_mark установлен в true, фаза маркировки проходит для обоих поколений, если в false, то только для младшего. Если immediate_sweep установлен в true, будет произведено полное немедленное удаление объектов (stop the world), если в false, то будет произведено «ленивое» удаление (lazy sweep), происходящее, только когда это необходимо и удаляющее необходимый минимум объектов. GC.start # trigger a full GC run GC.start (full_mark: false) # only collect young generation GC.start (immediate_sweep: false) # mark only GC.start (full_mark: false, immediate_sweep: false) # minor GC Опция отладки GC.stress теперь может быть установлена как целочисленный флаг, задающий, какая часть сборщика должна быть усилена. GC.stress = true # full GC at every opportunity GC.stress = 1 # minor marking at every opportunity GC.stress = 2 # lazy sweep at every opportunity GC.stat теперь выводит больше деталей, а также может получать на вход параметр и возвращать значение только для этого ключа, вместо вывода всего хэша значений. GC.stat #=> {: count=>6, … } GC.stat (: major_gc_count) #=> 2 GC.stat (: minor_gc_count) #=> 4 Также появился метод latest_gc_info, возвращающий информацию о последнем запуске сборщика мусора. GC.latest_gc_info #=> {: major_by=>: oldgen, : gc_by=>: newobj, : have_finalizer=>false, : immediate_sweep=>false} GC — настройка переменных окружения Было введено множество новых переменных окружения, которые учитываются при работе сборщика мусора в Ruby.RUBY_GC_HEAP_INIT_SLOTS Эта опция была ранее доступна как RUBY_HEAP_MIN_SLOTS. Она задает изначальное расположение слотов и по умолчанию установлена в 10000.RUBY_GC_HEAP_FREE_SLOTS Эта опция также ранее была доступна как RUBY_FREE_MIN. Она задает минимальное количество слотов, которое должно быть доступно после запуска GC. Если GC освободил недостаточно слотов, будут выделены новые.По умолчанию — 4096.RUBY_GC_HEAP_GROWTH_FACTOR Задает коэффициент, по которому будет расти число выделяемых слотов. (next slots number) = (current slots number) * (this factor). По умолчанию — 1.8.RUBY_GC_HEAP_GROWTH_MAX_SLOTS Максимальное количество слотов, выделяемых за один раз. По умолчанию 0, что означает, что максимум не задан.RUBY_GC_MALLOC_LIMIT Эта опция не новая, но заслуживает упоминания. Она задает объем памяти, который может быть выделен без обращения к сборке мусора.По умолчанию 16×1024 * 1024 (16MB).RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR Коэффициент увеличения malloc_limit, по умолчанию 1.4.RUBY_GC_MALLOC_LIMIT_MAX Максимум, который может достичь malloc_limit. По умолчанию 32×1024 * 1024 (32MB).RUBY_GC_OLDMALLOC_LIMIT Объем, которого может достичь старшее поколение до вызова полной сборки мусора. По умолчанию 16×1024 * 1024 (16MB).RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR Коэффициент увеличения old_malloc_limit. По умолчанию 1.2.RUBY_GC_OLDMALLOC_LIMIT_MAX Максимум, который может достичь old_malloc_limit. По умолчанию 128×1024 * 1024 (128MB).Класс ObjectSpace для отслеживания утечек памяти Ruby 2.1 добавляет несколько инструментов для отслеживания ситуаций, когда мы оставляем ссылки на старые/большие объекты, не давая тем самым к ним доступ сборщику мусора.Теперь мы получили набор методов для определения размещения объекта:
require «objspace»
module Example class User def initialize (first_name, last_name) @first_name, @last_name = first_name, last_name end
def name »#{@first_name} #{@last_name}» end end end
ObjectSpace.trace_object_allocations do obj = Example: User.new («Arthur», «Dent»).name ObjectSpace.allocation_sourcefile (obj) #=> «example.rb» ObjectSpace.allocation_sourceline (obj) #=> 10 ObjectSpace.allocation_class_path (obj) #=> «Example: User» ObjectSpace.allocation_method_id (obj) #=>: name ObjectSpace.allocation_generation (obj) #=> 6 end Число, возвращаемое методом allocation_generation, это количество сборок мусора, прошедших на момент создания объекта. Т.о., если это число мало, то объект был создан во время начала работы приложения.Также есть методы trace_object_allocations_start и trace_object_allocations_stop в качестве альтернативы использованию trace_object_allocations с передачей блока, и метод trace_object_allocations_clear для сброса записанных данных о размещении объектов.
Кроме того, можно выводить эту и не только информацию в файл или строку в формате JSON для дальнейшего анализа или визуализации.
require «objspace»
ObjectSpace.trace_object_allocations do puts ObjectSpace.dump ([«foo»].freeze) end выведет { «address»:»0×007fd122123f40», «class»:»0×007fd121072098», «embedded»: true, «file»: «example.rb», «flags»: { «wb_protected»: true }, «frozen»: true, «generation»: 6, «length»: 1, «line»: 4, «references»: [ »0×007fd122123f68» ], «type»: «ARRAY» } Также возможно использовать метод ObjectSpace.dump_all для получения информации обо всей памяти в куче. require «objspace» ObjectSpace.trace_object_allocations_start # do things … ObjectSpace.dump_all (output: File.open («heap.json», «w»)) Оба этих метода могут быть использованы без активации object allocation tracing, но при этом будет получено меньше информации.И наконец, есть метод ObjectSpace.reachable_objects_from_root, который аналогичен методу ObjectSpace.reachable_objects_from, но он не принимает аргументов и работает от корня приложения. У этого метода есть одна особенность — он возвращает хэш, который при доступе по ключам сравнивает их на идентичность, соответственно для доступа вам нужны не просто те же строки, а именно те же объекты с тем же object_id, что и в самом хэше. К счастью это можно обойти:
require «objspace»
reachable = ObjectSpace.reachable_objects_from_root reachable = {}.merge (reachable) # workaround compare_by_identity reachable[«symbols»] #=> [«freeze», «inspect», «intern», … Первая часть на этом завершена, следующая будет через пару дней, в ней будут refinements, новые rubygems, изменения в лямбдах и еще много всего.