しょんぼり技術メモ

まいにちがしょんぼり

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の差はしばらく無視しようと思いました。