首先,我知道extend
和include
是如何工作的,以及它们通常用于什么等等。这是不是一个好主意不是我的问题的一部分。
我的问题是:extend
有多贵?这是扩展实例和单例对象的常用Javascript技术。人们可以在Ruby中做类似的事情,但如果在很多对象上使用它会慢吗?
答案 0 :(得分:23)
如果你在一个对象上调用extend
,让我们看看Ruby 1.9.3-p0会发生什么:
/* 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 )。如果之前访问过单例类(因此已经存在),那么会立即返回。如果没有,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)
,因为它需要检查模块是否存在已经存在(单例类中通常没有很多包含的模块,所以这没关系。)
结论: extend
不是一项非常昂贵的操作,因此如果您愿意,可以经常使用它。我唯一可以想象的是,在<{em> extend
之后调用实例的方法可能会稍微复杂一点,因为需要检查一个额外的模块层。如果你知道单例类已经存在,那么两者都不是问题。在这种情况下,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
的速度是其两倍多。 Object#define_singleton_method
是最慢的(尽管如果您只想动态添加单个方法,它可以更清晰。)
最有趣的结果是底部的两个,但是:创建一个对象然后扩展它只是创建对象的速度的近4倍!因此,如果您在循环中创建了大量的一次性对象,例如,如果扩展其中的每一个,它可能会对性能产生重大影响。在这里创建一个包含mixin的临时类是非常有效的。
答案 1 :(得分:8)
需要注意的一点是,extend(和include)都会重置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)
因此交错调用较慢。我不能确定唯一的原因是方法查找缓存被清除了。
答案 2 :(得分:1)
调用extend
使Ruby的所有方法缓存无效,无论是全局还是内联。这是任何时候你扩展任何类/对象所有方法缓存都被刷新并继续任何方法调用将达到冷缓存。
为什么这很糟糕,什么是方法缓存用于?
方法缓存用于在运行Ruby程序时节省时间。例如,如果您调用value.foo
,运行时将添加一个内联缓存,其中包含有关最新类value
的信息以及类层次结构foo
中的位置。这有助于加快来自同一个呼叫站点的未来呼叫。
如果在程序运行时经常扩展类/对象,它将变得非常慢。最好将扩展的类/对象限制在程序的开头。
这同样适用于定义方法和可能影响方法解析的任何其他更改。
有关此事的更多信息请参阅已故詹姆斯·戈利克的这篇文章,http://jamesgolick.com/2013/4/14/mris-method-caches.html