Rubyでifやcaseで頻繁に評価されるものに対して小手先の工夫をしてもあまり意味がなかった
タイトルで完結シリーズ。
ある比較処理を頻繁に実行するとする。その比較対象が、ほとんどある値を取る場合、その値を優先的に評価してやると高速になるんじゃないか?と思い立って実験してみた。linuxのコードに出てくるif(likely(cond))やif(unlikely(cond))みたいな…のとはちょっと違うけど。
こんなコードで実験してみると:
equire 'benchmark' SOME_CONST1 = "some const value1" SOME_CONST2 = "some const value2" SOME_CONST3 = "some const value3" SOME_CONST4 = "some const value4" def do_nothing(arg) return 0 end def do_it_by_if(arg) if arg == SOME_CONST1 return 0 end case arg when SOME_CONST2 return 1 when SOME_CONST3 return 2 else return 3 end end def do_it_by_case(arg) return case arg when SOME_CONST1 0 when SOME_CONST2 1 when SOME_CONST3 2 else 3 end end def do_it_by_case_quick_return(arg) case arg when SOME_CONST1 return 0 when SOME_CONST2 return 1 when SOME_CONST3 return 2 end return 3 end num_try = 5_000_000 puts "TestCase1: returns 1st value..." v = "some_const_value1" Benchmark.bmbm(8) do |x| x.report("nothing") { num_try.times { do_nothing(v) } } x.report("if") { num_try.times { do_it_by_if(v) } } x.report("case") { num_try.times { do_it_by_case(v) } } x.report("case-ret"){ num_try.times { do_it_by_case_quick_return(v) } } end puts puts "TestCase2: returns 2nd value..." v = "some const value2" Benchmark.bmbm(8) do |x| x.report("nothing") { num_try.times { do_nothing(v) } } x.report("if") { num_try.times { do_it_by_if(v) } } x.report("case") { num_try.times { do_it_by_case(v) } } x.report("case-ret"){ num_try.times { do_it_by_case_quick_return(v) } } end
こうなった。
$ ruby -v ./case-test.rb ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux] TestCase1: returns 1st value... Rehearsal -------------------------------------------- nothing 0.890000 0.000000 0.890000 ( 0.890456) if 2.390000 0.010000 2.400000 ( 2.397222) case 2.470000 0.000000 2.470000 ( 2.481626) case-ret 2.400000 0.000000 2.400000 ( 2.412282) ----------------------------------- total: 8.160000sec user system total real nothing 0.840000 0.000000 0.840000 ( 0.836868) if 2.390000 0.000000 2.390000 ( 2.396136) case 2.470000 0.000000 2.470000 ( 2.478274) case-ret 2.400000 0.000000 2.400000 ( 2.417253) TestCase2: returns 2nd value... Rehearsal -------------------------------------------- nothing 0.840000 0.000000 0.840000 ( 0.837193) if 2.150000 0.000000 2.150000 ( 2.156792) case 2.110000 0.000000 2.110000 ( 2.119092) case-ret 2.100000 0.000000 2.100000 ( 2.106889) ----------------------------------- total: 7.200000sec user system total real nothing 0.830000 0.000000 0.830000 ( 0.839534) if 2.150000 0.000000 2.150000 ( 2.156893) case 2.110000 0.000000 2.110000 ( 2.118204) case-ret 2.100000 0.000000 2.100000 ( 2.105491)
nothingは関数呼び出しのオーバヘッドを見るためのもの。常に0を返すだけ。
ifが「頻繁に出てくる条件を先にif文で評価(満たせば即return)してから、残りをcaseで評価」するパターン。
caseが「全パターンをcaseで評価。その後caseの返値をreturn」するパターンで、case-retが「全パターンをcaseで評価、満たした場合は即return」するパターン。
TestCase1は、「先にifで評価される値を引数にして500万回呼ぶ」テストケース。
TestCase2は、「そうではなく、caseで2番目に評価される値を引数にして500万回呼ぶ」テストケース。
TestCase1では、ifのパターンがcaseのパターンより0.08秒早い。TestCase2では、ifがcaseのパターンより0.04秒ほど遅いという結果になった。
1回あたり160マイクロ秒の差が出ることになる。
もっとも、そもそもこの測定があまり安定しない(想定した結果にならないことがたまにある)し、ifで優先的に書いたコードは読みにくいので、160usの差はしばらく無視しようと思いました。