GC負荷を制御する

ちょっと試したら意外に効果が得られたので書いておく。


Rubyにおいて、特に一定時間内に処理を行う必要のあるアクションゲームなどを作る場合には、GCの処理で一瞬止まるのが大きな問題となる。
GCの対策として打てる手は3つ。
(a) 毎フレームGCを動かす
(b) GCが動かないよう、オブジェクトを生成しないコードを徹底する
(c) 負荷が低いときを狙ってGCを動かす


簡単なのは(a)だが、GCの負荷はバカにならないので、あまりオススメできない。まとめてGCする処理落ちは防げても、動作可能最低環境を大きく引き上げることになる。
(b)は凄まじい効果があるが、作るのは大変だ。まずFloatを使わず固定小数点にして整数のみにし、キャラのオブジェクトもあらかじめ生成しておいて未使用オブジェクト用配列に入れておく。その他、動的に配列が生成されるような記述も控えて、文字列は使わず、三角関数もテーブル化する。などなど。記述がかなり制限され、Rubyの柔軟性は削られる。
これはGCがどうこうと言うよりも、オブジェクトの生成と破棄を完全になくすという最適化であり、副作用としてGCが動かなくなる。まあ、そこまでするならRuby以外の言語を使えよ、と言われるかもしれないレベルの話だ。
そういうのが好きなら是非チャレンジしてみてほしい。
もっと手軽に効果のある方法として、(c)が挙げられる。例として以下のコードを。この間から書いてるキャラ移動・弾幕生成プログラムに組み込んでみたサンプル。

gcload1 = 50
gcload2 = 50
i = 0
c = 0
Window.loop do
  Objects.each do |obj| obj.update end
  Objects.delete_if do |obj| obj.delete end
  Objects.each do |obj| obj.draw end
  Window.drawFont(0, 0, Window.getLoad.to_i.to_s + " %", font, :z => 100.0)
  Window.drawFont(0, 32, Objects.size.to_s + " objects", font, :z => 100.0)

  i = 1 - i
  if i == 0 and gcload1 - gcload2 + Window.getLoad < 90
    GC.start
    gcload1 = (gcload1 + Window.getLoad) / 2
  else
    gcload2 = (gcload2 + Window.getLoad) / 2
  end

  puts "over #{c}" if Window.getLoad > 100.0
  c += 1
end

1フレームおきにGC.startを動かし、なんとなく平均っぽい負荷を計算しておく。現在の負荷と、GC有無の差分を足して90%を超えたらGC起動による処理落ちの可能性ありとして動かさない。
負荷の上昇を検知してGCを控え、負荷が下がった瞬間にGCが動く。
負荷が低いときにGCを動かしておけばオブジェクト数は少ないから、短時間の負荷上昇期間にGCが動く可能性は低く、GCが動く前に負荷が下がれば処理落ちは発生しない。
もちろん、負荷がずっと高いと自動で起動されたGCにより処理落ちは発生するが、それはそもそもPCの速度が根本的に足りていない。
この処理は最適化ではなく、GCを制御して処理落ちをなるべく少なくするだけだ。
最後に、思いつきでテキトーに作ったので致命的にダメなロジックである可能性があることを付け加えておく。