ハッシュのキーにシンボル、は速い?

RHGを少し読んでみた。第1部の第2章を半分ぐらい。分からないことだらけだけど、どうも鼻血が出そうにおもしろそうだということが分かった。文章も謎解き風で、非常におもしろいし、まじめなのか不真面目なのか分からないようなノリが楽しい。

ところでQnilの「Q」は何だろう。RならわかるがなぜQなのか。聞いてみたところ答えは「Emacsがそうだったから」だそうだ。意外に楽しくなかった。

とか思わず画面の前で吹いてしまった。ruby.hには、以下のように真偽値の定義がある。

#define Qfalse 0        /* Rubyのfalse */
#define Qtrue  2        /* Rubyのtrue */
#define Qnil   4        /* Rubyのnil */

ぼくはこれをパッと見たとき、何となくQはQuadのQじゃないかと思った。ともあれ、これであちこちのメソッド定義関数にあるreturn Qtrueとかいうのが何を返しているのかが具体的に分かった。

で、ソースのあちこちに書いてあるVALUEはオブジェクトのポインタ(にキャストされることになる32bitのunsigined long)ということが分かった。そして、シンボルというのは、小さな整数とかと同様に、このVALUEに直接埋め込まれているものらしい。小さな数値を扱うだけでオブジェクトは大げさでコストが高いってことか。うーん、良く分からないけど、VALUEとは別にIDというやっぱり32bitの値があって、それは文字列と1対1に対応している。で、それが表出したものがシンボルという。

ハッシュのキーにシンボルを使うといいよという話だったけど、それってつまりハッシュ値を計算していないってことなんだろうか。じゃあ、そもそもハッシュじゃないような気もするけど、使う側にとってハッシュの本質はキーの文字列が一意に何かをポイントしてくれるだから、それでいいってことなのかな。

とりあえず実行速度を計ってみた。マシンはCore 2 Duoの1.86GHzとか。

s1.rb

h = {:apple => 1, :banana => 2, :citrus => 3}
1000000.times {
  h[:apple] += 1
  h[:banana] += 1
  h[:citrus] += 1
}
puts "apple: #{h[:apple]}"
puts "banana: #{h[:banana]}"
puts "citrus: #{h[:citrus]}"

s2.rb

h = {"apple" => 1, "banana" => 2, "citrus" => 3}
1000000.times {
  h["apple"] += 1
  h["banana"] += 1
  h["citrus"] += 1
}
puts "apple: #{h["apple"]}"
puts "banana: #{h["banana"]}"
puts "citrus: #{h["citrus"]}"
$ time ruby s1.rb 
apple: 1000001
banana: 1000002
citrus: 1000003

real    0m2.560s
user    0m2.404s
sys     0m0.124s
$ time ruby s2.rb 
apple: 1000001
banana: 1000002
citrus: 1000003

real    0m4.532s
user    0m4.320s
sys     0m0.168s
$ 

なるほど、確かにシンボルを使ったほうがだいぶ速い……、けど、まあよほどでないと、どうでもいい違いだ。ハッシュのキーにシンボルを使うのは、えーと、意図を明確にする意味があるとか?

Ruby1.9でもやってみた。

$ time ~/src/ruby/ruby s1.rb 
apple: 1000001
banana: 1000002
citrus: 1000003

real    0m1.023s
user    0m0.992s
sys     0m0.012s
$ time ~/src/ruby/ruby s2.rb 
apple: 1000001
banana: 1000002
citrus: 1000003

real    0m3.040s
user    0m3.000s
sys     0m0.020s
$

うーむ、1.8より1.9のほうが差が大きい。ハッシュの計算ってCで書かれてて、1.8でも1.9でも、あんまり差がつかない比較的重たい処理だから、とか?