ハッシュ引数

Rubyではメソッドの引数の最後がハッシュの場合、{}を省略することができる。
これをキーワード引数のように使うことで、柔軟なメソッドを作れるようになっている。
DXRubyでも、Window.drawExはハッシュを引数にすることで各種効果を自由に取捨選択してつけることができるようにしている。
それとは別に単体の効果をつけるメソッドもそれぞれ用意してあるのは、ハッシュ引数を読み取るのに時間がかかっていたためだ。いちいちハッシュで指定するのも面倒だったのもあるが。
DXRuby1.0.8ではrb_hash_lookup関数を使うことで読み取り速度、すなわちWindow.drawExやWindow.drawFontの実行速度が大幅に改善された。代わりにRuby1.8.6以前では動かなくなってしまっていて、これはリリース後に気づいた。次で修正しようとは思っているが、修正する価値があるかどうかはわからない。


さて、Rubyでは配列や文字列、ハッシュについては、「評価時に毎回生成される」というお約束がある。
どういうことかというと、

3.times do
  p [1,2,3].pop # => 3
                #    3
                #    3
end

こんな感じで、3回実行したからと言って配列が空っぽになるわけではなく、毎回[1,2,3]が生成されてからpopされる、ということだ。考えてみれば当たり前に自然なことだ。


これを逆に考えると、配列、文字列、ハッシュは実行される部分に記述すると、実行されるたびにオブジェクトを生成してしまう、ということになる。
オブジェクトの生成は基本的な負荷となるので、中身が固定であれば定数やクラス変数にでも放り込んで毎回生成されないようにすると、少し速くなる。
そして、ここで初めの話に繋がるのだが、メソッドのハッシュ引数も、直接書くと毎回ハッシュオブジェクトが生成される。実行速度を気にするのであれば、ここも気をつけないといけない。
実際にどれぐらい違いが出るのかを試してみよう。
環境はruby 1.9.1p378 (2010-01-10 revision 26273) [i386-mingw32]だ。

require 'Benchmark'

def hash_key_test(val)
end

puts Benchmark.measure {
  for i in 0..1000000
    hash_key_test(:aaa=>1)
  end
}
puts Benchmark.measure {
  data = {:aaa=>1}
  for i in 0..1000000
    hash_key_test(data)
  end
}

この2つはどちらも{:aaa=>1}というハッシュオブジェクトをhash_key_testに渡している。違いは書き方だけだ。
気になる結果は、

  1.735000   0.031000   1.766000 (  1.796875)
  0.390000   0.000000   0.390000 (  0.390625)

遅いPCだと200〜300個程度のオブジェクトを描画するWindow.drawExの呼び出しだけで1msぐらい余分にかかってしまいそうな勢いだ。もちろんキーの数が増えれば差は広がる一方だということも付け加えておこう。


ちなみに、DXRubyで画像を大量に描画するだけのプログラムを動かしてみたところ、1.8.7では3割ほどの速度低下が見られたのだが、1.9.1ではまったく変わらなかった。
上記のベンチでは確かに差があるから、描画処理で頭打ちになっているのだと考えられる。
普通にゲームを作るともっとRubyでの処理が増えて、そうなってくると描画よりもRuby処理のほうに時間がかかってくるのではないかとは思うのだが、シンプルなプログラムだとは言えDXRubyの描画がネックになってしまうというのはショックだった。
Ruby1.9.1、侮れない。