Подтвердить что ты не робот

Насколько дорого стоит "рубина"?

Прежде всего, я знаю, как работают extend и include, и то, что они обычно используют и т.д. Является ли это хорошей идеей или нет, не является частью моего вопроса.

Мой вопрос: насколько дорога extend? Это обычный метод Javascript для расширения экземпляров и одиночных объектов. Можно было сделать что-то подобное в Ruby, но будет ли оно медленным, если оно используется на множестве объектов?

4b9b3361

Ответ 1

Посмотрите, что произойдет в Ruby 1.9.3-p0, если вы вызываете extend для объекта:

/* eval.c, line 879 */
void
rb_extend_object(VALUE obj, VALUE module)
{
    rb_include_module(rb_singleton_class(obj), module);
}

Таким образом, модуль смешается в одноэлементный класс объекта. Насколько дорого стоить одноэлементный класс? Ну, rb_singleton_class_of(obj) в свою очередь вызывает singleton_class_of(obj) (class.c: 1253). Это немедленно возвращается, если к классу Singleton был получен доступ (и, следовательно, уже существует). Если нет, новый класс создается make_singleton_class, что тоже не слишком дорого:

/* class.c, line 341 */
static inline VALUE
make_singleton_class(VALUE obj)
{
    VALUE orig_class = RBASIC(obj)->klass;
    VALUE klass = rb_class_boot(orig_class);

    FL_SET(klass, FL_SINGLETON);
    RBASIC(obj)->klass = klass;
    rb_singleton_class_attached(klass, obj);

    METACLASS_OF(klass) = METACLASS_OF(rb_class_real(orig_class));
    return klass;
}

Это все O(1). После этого вызывается rb_include_module (class.c: 660), который O(n) относится к числу модулей, уже включенных в класс singleton, потому что ему нужно проверить, есть ли модуль уже там ( обычно не будет много включенных модулей в одноэлементном классе, так что это нормально).

Вывод: extend не очень дорогая операция, поэтому вы можете часто ее использовать, если хотите. Единственное, что я мог себе представить, это то, что разрешение метода вызывает экземпляр после extend может быть немного сложнее, так как необходимо проверять один дополнительный уровень модулей. Оба являются проблемой, если вы знаете, что класс singleton уже существует. В этом случае extend вводит почти никакой дополнительной сложности. Однако динамически расширяющиеся экземпляры могут привести к очень нечитаемому коду при слишком большом применении, поэтому будьте осторожны.

Этот небольшой тест демонстрирует ситуацию с производительностью:

require 'benchmark'

module DynamicMixin
  def debug_me
    puts "Hi, I'm %s" % name
  end
end

Person = Struct.new(:name)

def create_people
  100000.times.map { |i| Person.new(i.to_s) }
end

if $0 == __FILE__
  debug_me = Proc.new { puts "Hi, I'm %s" % name }

  Benchmark.bm do |x|
    people = create_people
    case ARGV[0]
    when "extend1"
      x.report "Object#extend" do
        people.each { |person|
          person.extend DynamicMixin
        }
      end
    when "extend2"
      # force creation of singleton class
      people.map { |x| class << x; self; end }
      x.report "Object#extend (existing singleton class)" do
        people.each { |person|
          person.extend DynamicMixin
        }
      end
    when "include"
      x.report "Module#include" do
        people.each { |person|
          class << person
            include DynamicMixin
          end
        }
      end
    when "method"
      x.report "Object#define_singleton_method" do
        people.each { |person|
          person.define_singleton_method("debug_me", &debug_me)
        }
      end
    when "object1"
      x.report "create object without extending" do
        100000.times { |i|
          person = Person.new(i.to_s)
        }
      end
    when "object2"
      x.report "create object with extending" do
        100000.times { |i|
          person = Person.new(i.to_s)
          person.extend DynamicMixin
        }
      end
    when "object3"
      class TmpPerson < Person
        include DynamicMixin
      end

      x.report "create object with temp class" do
        100000.times { |i|
          person = TmpPerson.new(i.to_s)
        }
      end
    end
  end
end

Результаты

           user     system      total        real
Object#extend                             0.200000   0.060000   0.260000 (  0.272779)
Object#extend (existing singleton class)  0.130000   0.000000   0.130000 (  0.130711)
Module#include                            0.280000   0.040000   0.320000 (  0.332719)
Object#define_singleton_method            0.350000   0.040000   0.390000 (  0.396296)
create object without extending           0.060000   0.010000   0.070000 (  0.071103)
create object with extending              0.340000   0.000000   0.340000 (  0.341622)
create object with temp class             0.080000   0.000000   0.080000 (  0.076526)

Интересно, что Module#include в метаклассе на самом деле медленнее, чем Object#extend, хотя он делает то же самое (потому что нам нужен специальный синтаксис Ruby для доступа к метаклассу). Object#extend более чем в два раза быстрее, если класс singleton уже существует. Object#define_singleton_method является самым медленным (хотя он может быть более чистым, если вы хотите динамически добавлять только один метод).

Самые интересные результаты - это дно два: создание объекта, а затем его расширение почти в 4 раза медленнее, чем только создание объекта! Например, если вы создаете много сквозных объектов в цикле, это может оказать значительное влияние на производительность, если вы расширяете каждый из них. Здесь гораздо эффективнее создать временный класс, который включает в себя mixin.

Ответ 2

Одна вещь, о которой нужно знать, - это расширить (и включить) как reset кеш, который Ruby использует для реализации методов поиска из имен.

Я помню, что это упоминалось как потенциальная проблема производительности на сессии в railsconf несколько лет назад. Я не знаю, что такое фактическое влияние на производительность, и нападает на меня как на нечто сложное для сравнения в изоляции. Адаптировав контрольный показатель Niklas, я сделал

require 'benchmark'

module DynamicMixin
  def debug_me
    puts "Hi, I'm %s" % name
  end
end

Person = Struct.new(:name)

def create_people
  100000.times.map { |i| Person.new(i.to_s) }
end

if $0 == __FILE__
  debug_me = Proc.new { puts "Hi, I'm %s" % name }

  Benchmark.bm do |x|
    people = create_people

    x.report "separate loop" do
      people.each { |person|
        person.extend DynamicMixin
      }
      people.each {|p| p.name}
    end

    people = create_people

    x.report "interleaved calls to name" do
      people.each { |person|
        person.extend DynamicMixin
        person.name
      }

    end

  end
end

В первом случае я делаю все расширение, а затем прохожу через всех людей и вызываю метод .name. Очевидно, что кейс недействителен, но как только я назвал имя для первого лица, кеш будет нагреваться и никогда не простужаться

Во втором случае я чередую вызовы для продолжения и звонки на .name, поэтому кеш всегда холоден, когда я вызываю .name

Число, которое я получаю,

       user     system      total        real
separate loop  0.210000   0.030000   0.240000 (  0.230208)
interleaved calls to name  0.260000   0.030000   0.290000 (  0.290910)

Таким образом, чередующиеся вызовы медленнее. Я не могу быть уверен, что единственная причина заключается в том, что кеш-поиск метода очищается.

Ответ 3

Вызов extend делает недействительными кеши всех методов Ruby, как глобальных, так и встроенных. Это в любое время, когда вы расширяете любой класс/объект, все кеши методов очищаются и идут вперёд, любой вызов метода попадает в холодный кэш.

Почему это плохо и для чего используются кеши для методов?

Кэш-память метода используется для экономии времени при запуске программы Ruby. Например, если вы вызываете value.foo, среда выполнения добавит немного встроенного кеша с информацией о последнем классе value и где в иерархии классов foo будет найден. Это помогает ускорить будущие вызовы с одного и того же call-сайта.

Если вы часто расширяете классы/объекты во время работы вашей программы, это будет значительно медленнее. Лучше всего ограничить расширение классов/объектов до начала вашей программы.

То же самое относится к методу определения и любым другим изменениям, которые могут повлиять на разрешение метода.

За дополнительной информацией по этому вопросу обратитесь к этой статье покойного Джеймса Голика, http://jamesgolick.com/2013/4/14/mris-method-caches.html