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) #=> #<ArgumentError: missing keyword: length>
Как видно из примера выше, в некоторых ситуациях именованные аргументы необходимы для устранения неоднозначности, но для них нельзя подобрать подходящее значение по умолчанию. Теперь вам не нужно выбирать.
Оптимизация метода String#freeze
Т.к. строки в Ruby являются изменяемыми, каждый строковый литерал является новым объектом при каждом выполнении:
def env
"development"
end
# 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, так что 1/3 может быть записана в 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 #=> #<MyProject::ConnectionError: MyProject::ConnectionError>
e.cause #=> #<Errno::ECONNREFUSED: Connection refused - connect(2) for "example.com" port 80>
end
На данный момент первое исключение нигде не выводится и rescue не обращает внимание на причину возникновения, но наличие метода #cause может существенно помочь при отладке.
У исключений также появился метод #backtrace_locations, который почему-то недоступен в 2.0.0. Он возвращает объекты Thread::Backtrace::Location вместо строк, что дает более простой доступ к деталям бэктрейса.
Поколенческий сборщик мусора
В 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": "0x007fd122123f40",
"class": "0x007fd121072098",
"embedded": true,
"file": "example.rb",
"flags": {
"wb_protected": true
},
"frozen": true,
"generation": 6,
"length": 1,
"line": 4,
"references": [
"0x007fd122123f68"
],
"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, изменения в лямбдах и еще много всего.
Автор: rsludge