- PVSM.RU - https://www.pvsm.ru -
Ruby 2.1, последняя значимая версия Ruby (на момент написания поста), была выпущена в Рождество 2013, спустя всего лишь 10 месяцев после выхода 2.0.0. Она вышла с целым рядом изменений и улучшений, и данный пост в деталях описывает эти новшества.
С версией 2.1 Ruby переходит на новую схему изменения версий на базе Semantic Versioning [1].
Версии выходят по схеме 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.
В именованные аргументы [2], появившиеся в 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>
Как видно из примера выше, в некоторых ситуациях именованные аргументы необходимы для устранения неоднозначности, но для них нельзя подобрать подходящее значение по умолчанию. Теперь вам не нужно выбирать.
Т.к. строки в 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, что не ломает прямую и обратную совместимость, плюс к тому многие люди недолюбливают лишние изменения в синтаксисе.
Результатом объявления метода теперь является не 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 [3] для оборачивания метода в 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!
Множество классов, получивших метод #to_h [4] в 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) и некоторые приемы(такие как тэггирование исключений [5]) бесполезными ввиду соображений производительности.
Теперь это не является проблемой, 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 [6]. Он возвращает объекты 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.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}
Было введено множество новых переменных окружения, которые учитываются при работе сборщика мусора в Ruby.
Эта опция была ранее доступна как RUBY_HEAP_MIN_SLOTS. Она задает изначальное расположение слотов и по умолчанию установлена в 10000.
Эта опция также ранее была доступна как RUBY_FREE_MIN. Она задает минимальное количество слотов, которое должно быть доступно после запуска GC. Если GC освободил недостаточно слотов, будут выделены новые.По умолчанию — 4096.
Задает коэффициент, по которому будет расти число выделяемых слотов. (next slots number) = (current slots number) * (this factor). По умолчанию — 1.8.
Максимальное количество слотов, выделяемых за один раз. По умолчанию 0, что означает, что максимум не задан.
Эта опция не новая, но заслуживает упоминания. Она задает объем памяти, который может быть выделен без обращения к сборке мусора.По умолчанию 16 * 1024 * 1024 (16MB).
Коэффициент увеличения malloc_limit, по умолчанию 1.4.
Максимум, который может достичь malloc_limit. По умолчанию 32 * 1024 * 1024 (32MB).
Объем, которого может достичь старшее поколение до вызова полной сборки мусора. По умолчанию 16 * 1024 * 1024 (16MB).
Коэффициент увеличения old_malloc_limit. По умолчанию 1.2.
Максимум, который может достичь old_malloc_limit. По умолчанию 128 * 1024 * 1024 (128MB).
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
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/60023
Ссылки в тексте:
[1] Semantic Versioning: http://semver.org/
[2] именованные аргументы: http://globaldev.co.uk/2013/03/ruby-2-0-0-in-detail/#keyword_arguments
[3] Module#prepend: http://globaldev.co.uk/2013/03/ruby-2-0-0-in-detail/#id4
[4] #to_h: http://globaldev.co.uk/2013/03/ruby-2-0-0-in-detail/#_as_standard_for_convert_to_hash
[5] тэггирование исключений: http://globaldev.co.uk/2013/09/ruby-tips-part-3/#exceptions
[6] почему-то недоступен в 2.0.0: http://globaldev.co.uk/2013/03/ruby-2-0-0-in-detail/#optimised_backtrace
[7] Источник: http://habrahabr.ru/post/222941/
Нажмите здесь для печати.