红宝石的“延伸”成本有多高?

时间:2011-12-23 12:47:01

标签: ruby

首先,我知道extendinclude是如何工作的,以及它们通常用于什么等等。这是不是一个好主意不是我的问题的一部分。

我的问题是:extend有多贵?这是扩展实例和单例对象的常用Javascript技术。人们可以在Ruby中做类似的事情,但如果在很多对象上使用它会慢吗?

3 个答案:

答案 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