Ответ 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.