Ruby 2.5 は引数に &block を書いても速い!!! #megurorb
Meguro.rb#10 で「引数に &block
を書いても速い!!!」という素晴らしい改善について話してきた。
b.r-l.o の issue で言うとこちら。 Feature #14045: Lazy Proc allocation for block parameters
前提知識
block の呼び出し方 3 パターン
block をメソッドで使う場合、大きく分けてこの 3 パターンがあると思う。
# block を引数で受け取って、call で呼ぶ
def block_call_with_block_arg(&block)
block.call
end
# block を引数で受け取らずに yield で呼ぶ
def yield_without_block_arg
yield
end
# 引数で受け取った block を捨てて yield で呼ぶ
def yield_with_block_arg(&block)
yield
end
引数で受け取った block を捨てる?
一番下の「引数で受け取った block を捨てて yield で呼ぶ」というパターンがなぜ存在するのかと言うと、 メソッドシグネチャで、このメソッドがブロックを受け取れることを明示できる という点です。中を読まなくても分かるのはメリットがデカい。
ところが遅い
require "benchmark/ips"
Benchmark.ips do |x|
# メソッド定義は上に書いたので省略
x.report("with arg and call") {
block_call_with_block_arg { 1 + 1 }
}
x.report("without arg") {
yield_without_block_arg { 1 + 1 }
}
x.report("with arg") {
yield_with_block_arg { 1 + 1 }
}
x.compare!
end
$ ruby benchmark.rb
Warming up --------------------------------------
with arg and call 85.049k i/100ms
without arg 203.843k i/100ms
with arg 95.914k i/100ms
Calculating -------------------------------------
with arg and call 1.287M (±11.1%) i/s - 6.379M in 5.025664s
without arg 6.029M (±16.2%) i/s - 29.150M in 5.001207s
with arg 1.444M (±12.1%) i/s - 7.098M in 5.002793s
Comparison:
without arg: 6029135.4 i/s
with arg: 1444165.4 i/s - 4.17x slower
with arg and call: 1286794.1 i/s - 4.69x slower
仮引数 &block
があるだけで 4 倍遅い。 call
すると更に遅い。
このことから、「block を引数で受け取らずに yield で呼ぶ」のパターンを使うのが 理想的な Ruby のコードとされてしまっているのでした。
(ちなみに call
を呼ぶパターンは、Performance/RedundantBlockCall cop が警告します)
ユーザの声
僕です。「&block
を書いたら遅い」という現象を知ってからずっと言ってる気がします。
「バグ」という表現を用いるの、強いですね。
もちろん、逆の意見もあります。
コミッタの声
やっぱり難しいから遅いまま放置されてるんですよねぇ……。
これは本当に難しそうです。
分かりやすい解説。
難しさのあまり錯乱しています。
&block と書くと遅いことによる弊害
JRuby かどうかでメソッド定義ごと分けるの、面白コードだ……。
速くなった!
ささださんすごい!!!!
https://gist.github.com/ko1/9d58b59a9f89e3089d236473b56b9bae
gist のファイル名が良いですね。
https://bugs.ruby-lang.org/issues/14045 の
Proposal: Lazy Proc allocation for
To avoid this overhead, I propose lazy Proc creation for block parameters.
Ideas:
- At the beginning of method, a block parameter is nil
- If block parameter is accessed, then create a Proc object by given block.
- If we pass the block parameter to other methods like block_yield(&b) then don't make a Proc, but pass given block information.
We don't optimize b.call type block invocations. If we call block with b.call, then create Proc object.We need to hack more because Proc#call is different from yield statement (especially they can change $SAFE).
とある通り、触るまで Proc オブジェクトのアロケーションを遅延させるというアイディアです。
次のメソッドに pass するだけでも速いの、なるほどなぁ。
上に書いたベンチを流すと
$ ruby benchmark.rb
Warming up --------------------------------------
with arg and call 83.870k i/100ms
without arg 210.480k i/100ms
with arg 165.800k i/100ms
Calculating -------------------------------------
with arg and call 1.030M (±19.4%) i/s - 4.948M in 5.033583s
without arg 5.276M (±18.2%) i/s - 25.047M in 5.012327s
with arg 4.714M (±15.3%) i/s - 22.880M in 5.007125s
Comparison:
without arg: 5275892.6 i/s
with arg: 4713655.5 i/s - same-ish: difference falls within error
with arg and call: 1030325.9 i/s - 5.12x slower
と、実際速い。
今後の更なる発展?
def foo(&block)
yield
end
def foo(&b)
yield
end
と、使わないのに block
とか b
とかの仮引数名があるのが気持ち悪いし、名前に悩む人が出そうだし。
なので、捨てる前提でシグネチャとしての明示だけならば
def foo(&)
yield
end
と仮引数を省略してブロックを受け取ることだけ書けるようにしようという提案を見た気がする。(うろ覚え)
んだけど b.r-l.o 検索力が低くて見つけられない……=■●_
3 行まとめ
- Ruby 2.5 には「引数に
&block
を書いても速い!!!」という目玉機能があります - メソッドのシグネチャだけ見て判断できる書き方が遅くなくなったので積極的に使おう
- コミッタの方々の tweet を読んでおくと Ruby バージョンアップのときに楽しい
LT 王を取った
なんと賞品で ルビィのぼうけん コンピューターの国のルビィ サイン本をいただきました。
(これは前作 ルビィのぼうけん こんにちは!プログラミング のときの tweet)
僕は絵本はかさばるなぁという大人だったのですが、やっぱり絵本という媒体は手に取ってみると物理本が良いですねぇ。
まずは35歳児が童心に返ってページをめくります。ありがとうございます!