ようへいの日々精進XP

よかろうもん

Ruby の Hash キーへのアクセスで String と Symbol でどのくらいの差があるのか比較してみた

tl;dr

Hash キーへのアクセスは String よりも Symbol の方が速いことがあるよというのを本で読んだけど、その速さの違いはどれくらいなのか、又、歴代の Ruby バージョンでどの位の違いがあるのかを比較することにしました。


既に

試されている方がいらっしゃいますが、自分でも試してみらんといかんばいということで、そちらの記事を参考にしつつ試してみたいと思います。

上記の記事、とても参考になりました。ありがとうございます。


環境

OS

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS"

Ruby バージョン

rbenv を利用して以下のバージョンを用意。

$ rbenv versions
* system (set by /home/vagrant/git/ruby-tutorial/.ruby-version)
  1.9.3-p551
  2.1.7
  2.2.3

ベンチマークツール

github.com

詳細な使い方については README をご一読下さい。

検証用コード

以下のようなコードを用意しました。

require 'benchmark/ips'

HASH_01 = {'foo' => 'bar'}
HASH_02 = {:foo => 'bar'}

Benchmark.ips do |x|
  x.report('String') { HASH_01['foo'] }
  x.report('Symbol') { HASH_02[:foo] }
  x.compare!
end

ハッシュの入れ物自体は定数、特に大きな意味は無いのです。


試す

実行方法

以下のように rbenv で Ruby のバージョンを切り替えながら、同バージョンで 3 回実施しました。

$ rbenv local 1.9.3-p551
$ ruby benchmark.rb
Calculating -------------------------------------
              String   119.019k i/100ms
              Symbol   150.413k i/100ms
-------------------------------------------------
              String      4.741M (± 5.1%) i/s -     23.328M
              Symbol      8.673M (± 7.6%) i/s -     42.116M

Comparison:
              Symbol:  8672894.5 i/s
              String:  4740561.3 i/s - 1.83x slower

3 回実施した上で Comparison の結果を整理しました。

Ruby 1.9.3-p551

1 回目 2 回目 3 回目
Symbol(i/s) 8672894.5 8603563.7 8660111.2
String(i/s) 4740561.3 4732022.1 4736707.4
1.83x slower 1.82x slower 1.83x slower

Ruby 2.1.7

1 回目 2 回目 3 回目
Symbol(i/s) 9955217.0 10242990.0 9906651.8
String(i/s) 5418971.5 5427069.0 5330578.7
1.84x slower 1.89x slower 1.86x slower

Ruby 2.2.3

1 回目 2 回目 3 回目
Symbol(i/s) 10276582.7 10259725.6 10393689.3
String(i/s) 8713234.5 8732910.8 8633184.5
1.18x slower 1.17x slower 1.20x slower

計測により判ったこと

  • Symbol アクセスの方が速いことが判った
  • Ruby 1.9.3-p551 と Ruby 2.1.7 では 2 倍弱の差があったが、2.2.3 では 1.2 倍程度に差が縮まっているのが興味深い
  • Ruby のバージョンが上がるに連れて全体的に処理性能があがっている

おまけ

なぜ Symbol が速いのか

後で調べて書いてみたいが、以下の記事が参考になりそう。

stackoverflow.com

上記の記事より抜粋したのものを雑に意訳すると...

If you use a string as a Hash key, Ruby needs to evaluate the string and look at it's contents (and compute a hash function on that) and compare the result against the (hashed) values of the keys which are already stored in the Hash.

ハッシュキーとして文字列を利用する場合、まずは文字列の評価を行った上でその内容とハッシュキーの比較を行ったうえで結果を返す。

If you use a symbol as a Hash key, it's implicit that it's immutable, so Ruby can basically just do a comparison of the (hash function of the) object-id against the (hashed) object-ids of keys which are already stored in the Hash. (much faster)

対して、ハッシュキーとしてシンボルを利用する場合にはハッシュキーのオブジェクト ID を利用して比較を行う。オブジェクト ID は不変であることから文字列よりも早く検索、比較して結果を返すことが出来る。

さらに雑に解釈すると...

  • String はオブジェクト ID が変わってしまう
  • Symbol はオブジェクト ID が不変

ということなので Symbol で指定した方が早く結果を返すことが出来る。

(フワッとしている...汗)

実行ログ

$ rbenv local 1.9.3-p551
$ ruby benchmark.rb
Calculating -------------------------------------
              String   119.019k i/100ms
              Symbol   150.413k i/100ms
-------------------------------------------------
              String      4.741M (± 5.1%) i/s -     23.328M
              Symbol      8.673M (± 7.6%) i/s -     42.116M

Comparison:
              Symbol:  8672894.5 i/s
              String:  4740561.3 i/s - 1.83x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   120.142k i/100ms
              Symbol   149.733k i/100ms
-------------------------------------------------
              String      4.732M (± 3.7%) i/s -     23.428M
              Symbol      8.604M (± 7.2%) i/s -     41.925M

Comparison:
              Symbol:  8603563.7 i/s
              String:  4732022.1 i/s - 1.82x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   111.907k i/100ms
              Symbol   149.702k i/100ms
-------------------------------------------------
              String      4.737M (± 5.0%) i/s -     23.389M
              Symbol      8.660M (± 6.9%) i/s -     42.216M

Comparison:
              Symbol:  8660111.2 i/s
              String:  4736707.4 i/s - 1.83x slower
$ rbenv local 2.1.7
$ ruby benchmark.rb
Calculating -------------------------------------
              String   136.530k i/100ms
              Symbol   166.095k i/100ms
-------------------------------------------------
              String      5.419M (± 4.0%) i/s -     26.760M
              Symbol      9.955M (± 7.5%) i/s -     48.500M

Comparison:
              Symbol:  9955217.0 i/s
              String:  5418971.5 i/s - 1.84x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   145.433k i/100ms
              Symbol   174.928k i/100ms
-------------------------------------------------
              String      5.427M (± 3.9%) i/s -     26.905M
              Symbol     10.243M (± 6.5%) i/s -     50.029M

Comparison:
              Symbol: 10242990.0 i/s
              String:  5427069.0 i/s - 1.89x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   143.528k i/100ms
              Symbol   169.065k i/100ms
-------------------------------------------------
              String      5.331M (± 3.7%) i/s -     26.409M
              Symbol      9.907M (± 6.8%) i/s -     48.353M

Comparison:
              Symbol:  9906651.8 i/s
              String:  5330578.7 i/s - 1.86x slower
$ rbenv local 2.2.3
$ ruby benchmark.rb
Calculating -------------------------------------
              String   172.781k i/100ms
              Symbol   178.983k i/100ms
-------------------------------------------------
              String      8.713M (± 5.1%) i/s -     42.677M
              Symbol     10.277M (± 6.5%) i/s -     50.294M

Comparison:
              Symbol: 10276582.7 i/s
              String:  8713234.5 i/s - 1.18x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   180.932k i/100ms
              Symbol   188.602k i/100ms
-------------------------------------------------
              String      8.733M (± 5.2%) i/s -     42.881M
              Symbol     10.260M (± 5.3%) i/s -     50.357M

Comparison:
              Symbol: 10259725.6 i/s
              String:  8732910.8 i/s - 1.17x slower

$ ruby benchmark.rb
Calculating -------------------------------------
              String   161.864k i/100ms
              Symbol   169.272k i/100ms
-------------------------------------------------
              String      8.633M (± 6.5%) i/s -     42.247M
              Symbol     10.394M (± 5.8%) i/s -     50.612M

Comparison:
              Symbol: 10393689.3 i/s
              String:  8633184.5 i/s - 1.20x slower