📝

【Ruby 3.4 Advent Calender】文字列リテラルの hash 値がコンパイル時に計算される【16日目】

2024/12/16に公開

Ruby 3.4 Advent Calender 16日目の記事です。

これはなに

今年 2024年12月25日にリリースされる予定の Ruby 3.4 の新機能や変更点などを1つずつ紹介していく Advent Calender になります。
基本的には NEWS に載っている機能を紹介すると思うんですがここにない機能についても書くかもしれません。
また、記事を書いてる時点ではまだ Ruby 3.4 はリリースされる前なので Ruby 3.4 がリリースされた時点で機能が変わっている 可能性があるかもしれないので注意してください。
記事のまとめは ここを参照 してください。

文字列リテラルの hash 値がコンパイル時に計算されるようになる

文字列リテラルの hash 値がコンパイル時に計算されるようになる!と言われてもあんまりピンと来ないと思うんですが簡単にいうと次のようなコードが Ruby 3.4 から高速化されます。

hash = { "name" => "homu" }

# 文字列リテラルでアクセスするときに高速化する
hash["name"]

これは文字列リテラルの hash 値を『コンパイル時に』計算することでより高速に Hash へのアクセスが行えるようになります。
これにより hash["name"]hash[:name] のような Symbol でのアクセスと同等の速さにすることを目標としています。

と、言われてもやっぱりピンと来ないと思うので実際に比較してみました。
検証コードは [Feature #20415] Precompute literal String hash code during compilation に載っていたものです( report("dyn_symbol") 部分を少しいじってます。

require 'benchmark/ips'

hash = 10.times.to_h do |i|
  [i, i]
end

dyn_sym = "dynamic_symbol".to_sym
hash[:some_symbol] = 1
hash[dyn_sym] = 1
hash["small"] = 2
hash["frozen_string_literal"] = 2

Benchmark.ips do |x|
  x.report("symbol") { hash[:some_symbol] }
  x.report("dyn_symbol") { hash[dyn_sym] }
  x.report("small_lit") { hash["small"] }
  x.report("frozen_lit") { hash["frozen_string_literal"] }
  x.compare!(order: :baseline)
end

これを Ruby 3.3.6 で実行させると以下のような結果になります。

$ RBENV_VERSION=3.3.6 ruby test.rb
ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]
Warming up --------------------------------------
              symbol     2.413M i/100ms
          dyn_symbol     2.526M i/100ms
           small_lit     2.310M i/100ms
          frozen_lit     2.216M i/100ms
Calculating -------------------------------------
              symbol     23.973M (± 1.4%) i/s   (41.71 ns/i) -    120.670M in   5.034666s
          dyn_symbol     25.193M (± 1.3%) i/s   (39.69 ns/i) -    126.284M in   5.013564s
           small_lit     23.199M (± 1.0%) i/s   (43.11 ns/i) -    117.832M in   5.079745s
          frozen_lit     22.060M (± 1.6%) i/s   (45.33 ns/i) -    110.802M in   5.024116s

Comparison:
              symbol: 23972520.7 i/s
          dyn_symbol: 25192863.3 i/s - 1.05x  faster
           small_lit: 23199021.9 i/s - 1.03x  slower
          frozen_lit: 22059661.9 i/s - 1.09x  slower

文字列リテラルでアクセスしてる small_lit / frozen_litsymbol でアクセスしたときよりも遅いのがわかりますね。
次は 3.4-dev で実行したときの結果になります。

$ RBENV_VERSION=3.4-dev ruby test.rb
ruby 3.4.0dev (2024-12-14T09:01:19Z master 70f5c62af1) +PRISM [x86_64-linux]
Warming up --------------------------------------
              symbol     1.990M i/100ms
          dyn_symbol     2.268M i/100ms
           small_lit     2.545M i/100ms
          frozen_lit     2.543M i/100ms
Calculating -------------------------------------
              symbol     25.763M (± 1.6%) i/s   (38.82 ns/i) -    129.329M in   5.021254s
          dyn_symbol     22.599M (± 1.3%) i/s   (44.25 ns/i) -    113.394M in   5.018470s
           small_lit     25.412M (± 1.7%) i/s   (39.35 ns/i) -    127.233M in   5.008313s
          frozen_lit     25.482M (± 1.1%) i/s   (39.24 ns/i) -    129.707M in   5.090679s

Comparison:
              symbol: 25541658.1 i/s
           small_lit: 25710479.8 i/s - same-ish: difference falls within error
          frozen_lit: 25565855.5 i/s - same-ish: difference falls within error
          dyn_symbol: 25093826.8 i/s - same-ish: difference falls within error

symbol / dyn_symbol / small_lit / frozen_lit でほとんど差がないことがわかると思います。

明確に何かしら便利機能が追加された!ってわけではないですが何かしら恩恵がありそうな追加機能になります。
あと個人的にはこういう高速化のアイディアは好き。

内部的なことが気になる人は [Feature #20415] Precompute literal String hash code during compilation に詳しく書いてあるのでそちらを読んでみるとよいと思います。

関連

GitHubで編集を提案

Discussion