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 渡せるかどうかを yield の有無で判断せざるを得ないので読解が難しくなるという悩ましさとは付き合っていくしか無いのかなぁ。
— Takafumi ONAKA (@onk) March 27, 2017
僕です。「&block
を書いたら遅い」という現象を知ってからずっと言ってる気がします。
Rubyで明示的なブロック引数(&block)のほうが暗黙的なブロック引数(yield)より遅いのバグだと思うので誰かなんとかしてほしい。
— FUJI Goro (@__gfx__) October 5, 2017
「バグ」という表現を用いるの、強いですね。
&bを作ってyieldするコードは(わざわざ未使用なローカル変数が出現しており、bindingとかで使われるのではないかといった疑いを持って読む必要が発生し)意図が逆にわかりにくいのでレビューで見たら反対する
— k0kubun (@k0kubun) October 23, 2017
もちろん、逆の意見もあります。
コミッタの声
&blockを渡してhttps://t.co/A6pIfK5f8qするよりyieldする方がずっと速いのを何とかする方法に半日くらい悩んだ結果、めちゃくちゃ厳しいのでお前らyieldを書けという結論に至りました
— k0kubun (@k0kubun) October 21, 2017
やっぱり難しいから遅いまま放置されてるんですよねぇ……。
一番難しいのはどこかでevalされてスコープの外に変数が持ち出される可能性があることですね。evalは任意のタイミングで発生し得るしevalの文字列の中身はわからない。変数の形になってなければ問題にはならないけれど。
— Urabe, Shyouhei (@shyouhei) October 5, 2017
呼ぶときは yield でも、$g = block とかあるとアウト
— \_ko1 (@_ko1) October 21, 2017
これは本当に難しそうです。
変数にするとそのメソッドの外にそのProcオブジェクトを持っていけるので適当にコンテキストをラップしており、&block があるメソッドを呼び出す旅にProcオブジェクトのアロケートが走っており、それが大変遅いようです
— k0kubun (@k0kubun) October 21, 2017
分かりやすい解説。
解決策を思いつきました!!! yield を遅くすればいいんだ
— \_ko1 (@_ko1) October 6, 2017
難しさのあまり錯乱しています。
&block と書くと遅いことによる弊害
JRubyだと&blockのほうが速いらしいんでRubyの&block速くなったらこのコード消したい https://t.co/idRB3TTDRD
— Ryuta Kamizono (@kamipo) October 5, 2017
JRuby かどうかでメソッド定義ごと分けるの、面白コードだ……。
速くなった!
Ruby 2.5 の目玉機能を作っておいた https://t.co/DmNGJozbap
— \_ko1 (@_ko1) October 23, 2017
https://t.co/8MyhIkEmfp これが本命。
— \_ko1 (@_ko1) October 23, 2017
ささださんすごい!!!!
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
とかの仮引数名があるのが気持ち悪いし、名前に悩む人が出そうだし。
僕はブロックを引数で取るときは、さらにメソッドを呼び出して引数に渡すのを暗示している節があるので…。
— いっくん (@alpaca_tc) December 10, 2017
必ずブロック引数を明示しないといけないとなると、ブロック引数の命名や、呼び出し方(block.call or yield)とか考えなきゃいけないのも🤔
なので、捨てる前提でシグネチャとしての明示だけならば
def foo(&)
yield
end
と仮引数を省略してブロックを受け取ることだけ書けるようにしようという提案を見た気がする。(うろ覚え)
んだけど b.r-l.o 検索力が低くて見つけられない……=■●_
3 行まとめ
- Ruby 2.5 には「引数に
&block
を書いても速い!!!」という目玉機能があります - メソッドのシグネチャだけ見て判断できる書き方が遅くなくなったので積極的に使おう
- コミッタの方々の tweet を読んでおくと Ruby バージョンアップのときに楽しい
LT 王を取った
結果発表!最も票を集めたのは @onk さんでした!おめでとうございます!#megurorb pic.twitter.com/J92A777kiq
— Meguro.rb (@megurorb) December 19, 2017
なんと賞品で ルビィのぼうけん コンピューターの国のルビィ サイン本をいただきました。
優勝者の @onk さんには、 @yotii23 さんのサイン入りで「ルビィのぼうけん コンピューターの国のルビィ」をお贈りします!おめでとうございます!#megurorb pic.twitter.com/obfB74YPdG
— Meguro.rb (@megurorb) December 19, 2017
あ、あしたルビィのぼうけんのKindle出るらしいですよ。お子さんには物理本の方がいいと思うけど、中身読みたいけど絵本はかさばるなぁという大人にどうぞ。
— やきとりい (@yotii23) July 28, 2016
(これは前作 ルビィのぼうけん こんにちは!プログラミング のときの tweet)
僕は絵本はかさばるなぁという大人だったのですが、やっぱり絵本という媒体は手に取ってみると物理本が良いですねぇ。
まずは35歳児が童心に返ってページをめくります。ありがとうございます!