简体   繁体   English

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

[英]Why is Symbol#to_proc slower in Ruby 1.8.7?

Relative Performance of Symbol#to_proc in Popular Ruby Implementations states that in MRI Ruby 1.8.7, Symbol#to_proc is slower than the alternative in their benchmark by 30% to 130%, but that this isn't the case in YARV Ruby 1.9.2. Relative Performance of Symbol#to_proc in Popular Ruby Implementations states that in MRI Ruby 1.8.7, Symbol#to_proc is slower than the alternative in their benchmark by 30% to 130%, but that this isn't the case in YARV Ruby 1.9. 2.

Why is this the case?为什么会这样? The creators of 1.8.7 didn't write Symbol#to_proc in pure Ruby. 1.8.7 的创建者并没有在纯 Ruby 中编写Symbol#to_proc

Also, are there any gems that provide faster Symbol#to_proc performance for 1.8?此外,是否有任何 gem 可以为 1.8 提供更快的 Symbol#to_proc 性能?

(Symbol#to_proc is starting to appear when I use ruby-prof, so I don't think I'm guilty of premature optimization) (当我使用 ruby-prof 时,符号#to_proc 开始出现,所以我不认为我对过早优化有罪)

The to_proc implementation in 1.8.7 looks like this (see object.c ): 1.8.7 中的to_proc实现如下所示(参见object.c ):

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

Whereas the 1.9.2 implementation (see string.c ) looks like this:而 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;
    }
}

If you strip away all the busy work of initializing sym_proc_cache , then you're left with (more or less) this:如果您剥离了初始化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;
}

So the real difference is the 1.9.2's to_proc caches the generated Procs while 1.8.7 generates a brand new one every single time you call to_proc .所以真正的区别是 1.9.2 的to_proc缓存了生成的 Procs,而 1.8.7 每次调用to_proc时都会生成一个全新的。 The performance difference between these two will be magnified by any benchmarking you do unless each iteration is done in a separate process;除非每次迭代都在单独的过程中完成,否则您所做的任何基准测试都会放大这两者之间的性能差异; however, one iteration per-process would mask what you're trying to benchmark with the start-up cost.但是,每个进程一次迭代会掩盖您尝试用启动成本进行基准测试的内容。

The guts of rb_proc_new look pretty much the same (see eval.c for 1.8.7 or proc.c for 1.9.2) but 1.9.2 might benefit slightly from any performance improvements in rb_iterate . rb_proc_new的内容看起来几乎相同(参见eval.c用于 1.8.7 或proc.c用于 1.9.2)但 1.9.2 可能会从rb_iterate的任何性能改进中受益The caching is probably the big performance difference.缓存可能是最大的性能差异。

It is worth noting that the symbol-to-hash cache is a fixed size (67 entries but I'm not sure where 67 comes from, probably related to the number of operators and such that are commonly used for symbol-to-proc conversions):值得注意的是,符号到哈希缓存的大小是固定的(67 个条目,但我不确定 67 来自哪里,可能与运算符的数量有关,这些通常用于符号到过程的转换):

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

If you use more than 67 symbols as procs or if your symbol IDs overlap (mod 67) then you won't get the full benefit of the caching.如果您使用超过 67 个符号作为 proc,或者您的符号 ID 重叠(mod 67),那么您将无法获得缓存的全部好处。

The Rails and 1.9 programming style involves a lot of shorthands like: Rails 和 1.9 编程风格涉及很多简写,例如:

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

rather than the longer explicit block forms:而不是更长的显式块 forms:

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

Given that (popular) programming style, it makes sense to trade memory for speed by caching the lookup.鉴于(流行的)编程风格,通过缓存查找以换取 memory 的速度是有意义的。

You're not likely to get a faster implementation from a gem as the gem would have to replace a chunk of the core Ruby functionality.您不太可能从 gem 中获得更快的实现,因为 gem 必须替换一部分核心 Ruby 功能。 You could patch the 1.9.2 caching into your 1.8.7 source though.不过,您可以将 1.9.2 缓存修补到 1.8.7 源中。

The following ordinary Ruby code:以下普通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

Will make Ruby MRI 1.8.7 Symbol#to_proc slightly less slow than before, but not as fast as an ordinary block or a pre-existing proc.将使 Ruby MRI 1.8.7 Symbol#to_proc比以前慢一些,但不如普通块或预先存在的 proc 快。

However, it'll make YARV, Rubinius and JRuby slower, hence the if around the monkeypatch.但是,它会使 YARV、Rubinius 和 JRuby 变慢,因此在 Monkeypatch 周围使用if

The slowness of using Symbol#to_proc isn't solely due to MRI 1.8.7 creating a proc each time - even if you re-use an existing one, it's still slower than using a block.使用 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

For the full benchmark and code, see https://gist.github.com/1053502有关完整的基准和代码,请参阅https://gist.github.com/1053502

In addition to not caching proc s, 1.8.7 also creates (approximately) one array each time a proc is called.除了不缓存proc之外,1.8.7 还会在每次调用proc时(大约)创建一个数组。 I suspect it's because the generated proc creates an array to accept the arguments - this happens even with an empty proc that takes no arguments.我怀疑这是因为生成的proc创建了一个数组来接受 arguments - 即使使用没有 arguments 的空proc也会发生这种情况。

Here's a script to demonstrate the 1.8.7 behavior.这是一个演示 1.8.7 行为的脚本。 Only the :diff value is significant here, which shows the increase in array count.只有: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 }
}

Sample output:样品 output:

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}

It seems that merely calling a proc will create the array for every iteration, but a literal block only seems to create an array once.似乎仅调用proc将为每次迭代创建数组,但文字块似乎只创建一次数组。

But multi-arg blocks may still suffer from the problem:但是多参数块可能仍然会遇到这个问题:

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 }
}

Sample output.样品 output。 Note how we incur a double penalty in case #2, because we use a multi-arg block, and also call the proc .请注意,在案例 #2 中我们如何招致双重惩罚,因为我们使用了多参数块,并且还调用了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}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM