为什么在Ruby 1.8.7中Symbol#to_proc较慢?

流行Ruby实现中Symbol#to_proc的相对性能表明,在MRI Ruby 1.8.7中, Symbol#to_proc比其基准测试中的替代品慢30%到130%,但在YARV Ruby 1.9中并非如此。 2。

为什么会这样? 1.8.7的创建者没有在纯Ruby中编写Symbol#to_proc

另外,有没有为1.8提供更快的Symbol#to_proc性能的gem?

(当我使用ruby-prof时,符号#to_proc开始出现,所以我不认为我对过早优化感到内疚)

1.8.7中的to_proc实现看起来像这样(参见object.c ):

 static VALUE sym_to_proc(VALUE sym) { return rb_proc_new(sym_call, (VALUE)SYM2ID(sym)); } 

1.9.2实现(参见string.c )如下所示:

 static VALUE sym_to_proc(VALUE sym) { static VALUE sym_proc_cache = Qfalse; enum {SYM_PROC_CACHE_SIZE = 67}; VALUE proc; long id, index; VALUE *aryp; if (!sym_proc_cache) { sym_proc_cache = rb_ary_tmp_new(SYM_PROC_CACHE_SIZE * 2); rb_gc_register_mark_object(sym_proc_cache); rb_ary_store(sym_proc_cache, SYM_PROC_CACHE_SIZE*2 - 1, Qnil); } id = SYM2ID(sym); index = (id % SYM_PROC_CACHE_SIZE) << 1; aryp = RARRAY_PTR(sym_proc_cache); if (aryp[index] == sym) { return aryp[index + 1]; } else { proc = rb_proc_new(sym_call, (VALUE)id); aryp[index] = sym; aryp[index + 1] = proc; return proc; } } 

如果你剥夺了初始化sym_proc_cache所有忙碌工作,那么你或多或少地留下了这个:

 aryp = RARRAY_PTR(sym_proc_cache); if (aryp[index] == sym) { return aryp[index + 1]; } else { proc = rb_proc_new(sym_call, (VALUE)id); aryp[index] = sym; aryp[index + 1] = proc; return proc; } 

所以真正的区别在于1.9.2的to_proc缓存生成的Procs,而1.8.7每次调用to_proc时都会生成一个全新的to_proc 。 除非每次迭代都在一个单独的过程中完成,否则这两者之间的性能差异将被任何基准测试放大; 但是,每个进程的一次迭代会掩盖您尝试使用启动成本进行基准测试的内容。

rb_proc_new看起来几乎相同(请参阅eval.c for 1.8.7或proc.c for 1.9.2)但1.9.2可能会受益于rb_iterate任何性能改进。 缓存可能是最大的性能差异。

值得注意的是,符号到散列缓存是固定大小(67个条目,但我不确定67来自哪里,可能与运算符的数量有关,这些通常用于符号到进程的转换):

 id = SYM2ID(sym); index = (id % SYM_PROC_CACHE_SIZE) << 1; /* ... */ if (aryp[index] == sym) { 

如果您使用超过67个符号作为过程或如果您的符号ID重叠(mod 67),那么您将无法获得缓存的全部好处。

Rails和1.9编程风格涉及很多简写:

  id = SYM2ID(sym); index = (id % SYM_PROC_CACHE_SIZE) << 1; 

而不是更长的显式块forms:

 ints = strings.collect { |s| s.to_i } sum = ints.inject(0) { |s,i| s += i } 

鉴于(流行的)编程风格,通过缓存查找来交换内存以提高速度是有意义的。

您不太可能从gem获得更快的实现,因为gem必须替换核心Rubyfunction的一大块。 您可以将1.9.2缓存修补到1.8.7源代码中。

以下普通的Ruby代码:

 if defined?(RUBY_ENGINE).nil? # No RUBY_ENGINE means it's MRI 1.8.7 class Symbol alias_method :old_to_proc, :to_proc # Class variables are considered harmful, but I don't think # anyone will subclass Symbol @@proc_cache = {} def to_proc @@proc_cache[self] ||= old_to_proc end end end 

将使Ruby MRI 1.8.7 Symbol#to_proc比之前略慢,但不如普通块或预先存在的proc快。

然而,它会使YARV,Rubinius和JRuby变慢,因此围绕着monkeypatch。

使用Symbol#to_proc的缓慢不仅仅是因为MRI 1.8.7每次创建一个proc – 即使你重新使用现有的一个,它仍然比使用一个块慢。

 Using Ruby 1.8 head Size Block Pre-existing proc New Symbol#to_proc Old Symbol#to_proc 0 0.36 0.39 0.62 1.49 1 0.50 0.60 0.87 1.73 10 1.65 2.47 2.76 3.52 100 13.28 21.12 21.53 22.29 

有关完整的基准和代码,请参阅https://gist.github.com/1053502

除了不缓存proc ,1.8.7每次调用proc时也会创建(大约)一个数组。 我怀疑这是因为生成的proc创建了一个接受参数的数组 – 即使是一个不带参数的空proc也会发生这种情况。

这是一个演示1.8.7行为的脚本。 这里只有:diff值很重要,它显示了数组计数的增加。

 # this should really be called count_arrays def count_objects(&block) GC.disable ct1 = ct2 = 0 ObjectSpace.each_object(Array) { ct1 += 1 } yield ObjectSpace.each_object(Array) { ct2 += 1 } {:count1 => ct1, :count2 => ct2, :diff => ct2-ct1} ensure GC.enable end to_i = :to_i.to_proc range = 1..1000 puts "map(&to_i)" p count_objects { range.map(&to_i) } puts "map {|e| to_i[e] }" p count_objects { range.map {|e| to_i[e] } } puts "map {|e| e.to_i }" p count_objects { range.map {|e| e.to_i } } 

样本输出:

 map(&to_i) {:count1=>6, :count2=>1007, :diff=>1001} map {|e| to_i[e] } {:count1=>1008, :count2=>2009, :diff=>1001} map {|e| e.to_i } {:count1=>2009, :count2=>2010, :diff=>1} 

似乎只调用proc会为每次迭代创建数组,但是文字块似乎只创建一次数组。

但多arg块仍可能遇到此问题:

 plus = :+.to_proc puts "inject(&plus)" p count_objects { range.inject(&plus) } puts "inject{|sum, e| plus.call(sum, e) }" p count_objects { range.inject{|sum, e| plus.call(sum, e) } } puts "inject{|sum, e| sum + e }" p count_objects { range.inject{|sum, e| sum + e } } 

样本输出。 注意我们如何在#2的情况下产生双重惩罚,因为我们使用多arg块,并且还调用proc

 inject(&plus) {:count1=>2010, :count2=>3009, :diff=>999} inject{|sum, e| plus.call(sum, e) } {:count1=>3009, :count2=>5007, :diff=>1998} inject{|sum, e| sum + e } {:count1=>5007, :count2=>6006, :diff=>999}