2011-06-28 20 views
4

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ではそうではないことを示しています。Ruby 1.8.7でSymbol#to_procが遅いのはなぜですか?

これはなぜですか? 1.8.7の作成者は純粋なRubyにSymbol#to_procを書きませんでした。

さらに、1.8のSymbol#to_procのパフォーマンスを向上させる宝石はありますか?

(シンボル#のto_proc私はルビー-profのを使用するときに表示され始めているので、私は、私は時期尚早な最適化の有罪だとは思わない)

答えて

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を初期化するの忙しい仕事、この:1.8.7は、ブランドの新しいを生成しながら、

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キャッシュ生成手続きオブジェクトであります1回毎にto_procと呼んでいます。これらの2つのパフォーマンスの差は、各反復が別のプロセスで行われない限り、ベンチマークによって拡大されます。ただし、プロセスごとに1回反復すると、開始コストでベンチマークしようとしていることが隠されます。 rb_proc_new

根性はほとんど同じに見える(1.8.7または1.9.2のためのproc.cためeval.cを参照)が、1.9.2はrb_iterateのいずれかの性能向上から、わずかに受けることができます。おそらくキャッシュは大きなパフォーマンスの違いです。

シンボル・ハッシュ・キャッシュは固定サイズ(67エントリですが、どこから来たのか分かりませんが、シンボル・ハッシュ・キャッシュによく使用される演算子の数に関係します-PROCの変換):あなたは、シンボルIDの重複(MOD 67)procsのよう場合、または以上の67個の記号を使用している場合

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

は、あなたはキャッシュの完全な利益を得ることはありません。

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がRubyのコア機能のチャンクを置き換えなければならないため、より速く実装することはできません。 1.8.7のソースに1.9.2のキャッシュをパッチすることができます。

+0

そして、キャッシュが効果的に67個のバケツを持っていることによって動作し、あなたがハッシュにシンボルをオンにし、それが中に行くと思いバケットかを確認し、 'sym'は、そのバケット内にある場合、それはそれでPROCを使用していますそれ以外の場合はそのバケットを変更してそのシンボルと適切なprocを保存します。 –

+0

@Andrew:キャッシュには67バケット、バケットあたり2つの配列エントリがあります(最初のエントリはシンボル、2番目のエントリはprocです)。そして、はい、シンボルが一致しない場合、キャッシュはスロットを再利用します、私はその答えを明確にするために更新します。 –

+1

@Andrew:BTW、過去数日間のRuby内部のツアーのおかげです。 –

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 

はなく、早く通常のブロックまたは既存のPROCとして、以前よりRubyのMRIは1.8.7 Symbol#to_procわずかに少ない遅くなります。

しかし、それはYARV、Rubinius、JRubyを遅くします。したがって、ifは、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 

proc Sをキャッシュしないことに加えてhttps://gist.github.com/1053502

+0

1.9.2の 'to_proc'も1.8より2倍速いと言っていますか?ニース。 –

+0

@muが短すぎます:1.9.2では、 'to_proc'は十分な時間実行された場合、通常のブロックよりもさらに高速です。 1.9.2と1.8を比較してどのようにして、ベンチマークに含まれているがベンチマークとは関係のない他の部品など、カウンターファインディング要因についてどのように説明しましたか? –

+0

私はあなたが速度差の原因を切り離していたと考えましたので、おそらくどこから来たのか説明していただけです。私は間違っていると思った。 Procsとブロックの間の速度の差は興味深く予想外です。 –

1

を参照して、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を呼び出すと、すべての反復のための配列を作成することをようだが、文字通りのブロックは一度だけの配列を作成しているようです。

しかし、多引数ブロックはまだ問題に苦しむことがあります。

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

サンプル出力。マルチargブロックを使用し、procも呼び出しているので、#2の場合にダブルペナルティが発生することに注意してください。

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} 
関連する問題