【Ruby 3.4 Advent Calender】文字列リテラルの hash 値がコンパイル時に計算される【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_lit
が symbol
でアクセスしたときよりも遅いのがわかりますね。
次は 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 に詳しく書いてあるのでそちらを読んでみるとよいと思います。
Discussion