Pull to refresh

Ruby 2.1 в деталях (Часть 1)

Reading time10 min
Views19K
Original author: Mat Sadler

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, изменения в лямбдах и еще много всего.

Вторая часть Третья часть
Tags:
Hubs:
+43
Comments3

Articles

Change theme settings