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

Mar*_*sen 19 ruby

首先,我知道怎么extendinclude他们的工作,什么通常用于等等.无论是一个好主意与否是不是我的问题的一部分.

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

Nik*_* B. 24

让我们看看如果你调用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);
}
Run Code Online (Sandbox Code Playgroud)

因此模块被混合到对象的单例类中.获取单例类的成本有多高?那么,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;
}
Run Code Online (Sandbox Code Playgroud)

这就是全部O(1).之后,调用rb_include_module(class.c:660),这是O(n)关于单例类已经包含的模块数量,因为它需要检查模块是否已经存在(单例类中通常没有很多包含的模块,所以这没关系).

结论: extend这不是一个非常昂贵的操作,所以如果你愿意,你可以经常使用它.我能想象到的唯一的事情就是该方法的决议要求,以实例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
Run Code Online (Sandbox Code Playgroud)

结果

           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)
Run Code Online (Sandbox Code Playgroud)

有趣的是,Module#include在元类上实际上比Object#extend它慢,尽管它完全相同(因为我们需要特殊的Ruby语法来访问元类).Object#extend如果单例类已经存在,则快两倍多.Object#define_singleton_method是最慢的(虽然如果你只想动态添加一个方法,它可以更清晰).

最有趣的结果是底部的两个,但是:创建一个对象然后扩展它只是创建对象的速度的近4倍!因此,如果您在循环中创建了大量的一次性对象,例如,如果扩展其中的每一个,它可能会对性能产生重大影响.在这里创建一个包含mixin的临时类是非常有效的.


Fre*_*ung 9

需要注意的一点是,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
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,我做所有扩展,然后循环遍历所有人并调用.name方法.缓存失效显然仍然会发生,但是一旦我在第一个人身上调用了名字,缓存就会变暖并且永远不会变冷

在第二种情况下,我交替调用extend和call .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)
Run Code Online (Sandbox Code Playgroud)

因此交错调用较慢.我无法确定唯一的原因是方法查找缓存被清除了.