Relative Performance of Symbol#to_proc in Popular Ruby Implementations指出,在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性能(当我使用ruby-prof时,符号#to_proc开始出现,所以我认为我不会过早优化)
答案 0 :(得分:7)
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
时都会生成一个全新的Proc。除非每次迭代都在一个单独的过程中完成,否则这两者之间的性能差异将被任何基准测试放大;但是,每个进程的一次迭代会掩盖您尝试使用启动成本进行基准测试的内容。
rb_proc_new
的内容看起来几乎相同(1.8.7请参阅eval.c
或1.9.2请proc.c
)但1.9.2可能会从rb_iterate
。缓存可能是性能差异很大。
值得注意的是,符号到散列缓存是固定大小(67个条目,但我不确定67来自哪里,可能与运算符的数量有关,因此通常用于符号到-proc转换):
id = SYM2ID(sym);
index = (id % SYM_PROC_CACHE_SIZE) << 1;
/* ... */
if (aryp[index] == sym) {
如果您使用超过67个符号作为过程,或者您的符号ID重叠(模块67),那么您将无法获得缓存的全部好处。
Rails和1.9编程风格涉及很多简写:
id = SYM2ID(sym);
index = (id % SYM_PROC_CACHE_SIZE) << 1;
而不是更长的显式块形式:
ints = strings.collect { |s| s.to_i }
sum = ints.inject(0) { |s,i| s += i }
鉴于(流行的)编程风格,通过缓存查找来交换内存以提高速度是有意义的。
你不太可能从gem获得更快的实现,因为gem必须替换核心Ruby功能的一大块。您可以将1.9.2缓存修补到1.8.7源代码中。
答案 1 :(得分:4)
以下普通的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
稍微慢一些,但不如普通块或预先存在的过程快。
然而,它会使YARV,Rubinius和JRuby变慢,因此围绕monkeypatch if
。
使用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
答案 2 :(得分:1)
除了不缓存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}