Optcarrot: Ruby で書かれたファミコンエミュレータ

ウソみたいな本当の話。Rubyファミコンエミュレータを書いてみました。

気になる速度ですが、自分の環境では 20 fps ちょっと出ます。ファミコンは 60 fps なので、実速の 1/3 です。Ruby3x3 (Ruby 3 は Ruby 2 の 3 倍速い)という matz の宣言が実現すれば、実速が達成されることになりますね!

試してみたい人はこんなふうに実行してください。

$ gem install ffi
$ git clone http://github.com/mame/optcarrot.git
$ cd optcarrot
$ bin/optcarrot examples/Lan_Master.nes

SDL2 か SFML が適切にインストールされている必要があります。Debian/Ubuntu なら apt-get install libsdl2-dev で。

Ruby 処理系ベンチマークとして

optcarrot は --benchmark オプションを付けることで、ベンチマークモードになります。GUI 無しで 180 フレームを実行し、最後の 10 フレームの実行時間から fps を算出して終了します。

$ ruby bin/optcarrot --benchmark examples/Lan_Master.nes
fps: 34.25774482727466
checksum: 59662

いろんな Ruby 処理系で optcarrot --benchmark を走らせてみた結果がこちら。

MRI は 1.8 → 1.9 → 2.0 → 2.3 で確かに速まっているのがわかります。リリース時に "performance improvement" などと書かれているやつ、なかなか実感することはないですが、ウソじゃなかったんですね。MRI 高速化家の皆さんに敬礼。

JRuby や Rubinius は予想外に遅いですね。Topaz は「本家の 5 倍速?」ほどではないですが、健闘してます。*1追記:jruby-Xcompile.invokedynamic=true 使えばスタートアップ遅くなる代わりに処理速度が速くなる、とのことで、試したら MRI に匹敵しました。ちなみに jruby 9k のスナップショット版では MRI の 2 倍くらい出るようです。

IBM の OMR previewruby 2.2 ベースですが、2.2 より遅いんですよね。JIT の高速化より、プロファイルとコンパイルのオーバーヘッドの方が大きいようです。Ruby の場合、ボトルネックは評価器以外にあって、JIT で高速化する余地が相対的に少ないのかもしれません。

個人的に注目してほしいのは、mruby でも動いているところです *2 。Optcarrot はベンチマークプログラムを意識して書いたので、Ruby の基本的な機能(と自分が思うもの)しか使っておらず、外部ライブラリを一切使用しない *3 ので、移植が比較的容易です。なんと miniruby でも動くという、Ruby 処理系開発者に優しい仕様。

想定質問

なぜこんなものを作ったの?

Ruby3x3 を煽るため。OSS の開発を進めるには、開発者という馬を走らせるための「にんじん」が必要、というのは matz がたびたび言っていることです。そこで、その「にんじん」の 1 つとなればいいなと思って作りました。optcarrot は optimization carrot(最適化ニンジン)の略です。Ruby3x3 が達成されれば、開発者はファミコンゲームで遊べるというご褒美が得られます。

それから個人的に、遅い遅いと言われる Ruby の限界に挑戦してみたかったというのもあります。ファミコンエミュレータは 256 x 240 の画面を 60 fpsピクセル単位で描画する必要があります。しかし Ruby って、配列を 256 x 240 x 60 回更新するだけで 0.2 秒とか要するわけですよ。確かに遅い。残りの 0.8 秒で CPU シミュレーションとか音波合成とかしなきゃならない。無理ゲーです。この無理ゲーにどこまで応えられるか。最初にナイーブに書いたときは 3 fps とかだったんで、Ruby3x3 のストーリーに合うように 20 fps まで高速化しました。7 倍くらいの高速化は Ruby レベルの工夫次第でどうにでもなるということですね。

ごちゃごちゃ言ってるけど 60 fps でないんでしょ?

--opt を付ければ出るかも?

$ optcarrot --opt Lan_Master.nes

Optcarrot はベンチマークプログラムなので、なるべく普通で綺麗なコードを書くことにしたんですが、--opt を付けると綺麗さを犠牲にして高速化します。具体的には、まず自分自身のソースコードを読み込んで、メソッドインライン展開したり、簡単な部分計算したり、fastpath をこしらえたりして、コードクローンだらけの高速だけど最悪なコードを内部的に生成します。この処理は Ruby の得意とする文字列処理なんで、正規表現を駆使して適当にやってます。で、生成されたプログラム文字列を eval することでボトルネックの処理を置き換えます。これで自分のノートパソコンでは 60 fps を達成しました。めでたい。

まあ、こんなことは普通の Ruby プログラムではやるべきでないし、これをもって「Ruby で 60 fps 達成した!」と主張するのはちょっと何か違うような気もするので、おまけ機能です。60 fps 出たときはうれしかったけどね。前向きに言えば、MRI が今後何を最適化していくべきかを考える材料提供くらいにはなるかと。*4

--opt を含めたベンチマークはこちら。

MRI 以外速くならないですね。このモードでは巨大な case 文を内部生成するんですが、case 文をジャンプテーブル的に最適化するのって MRI だけぽいのでした。*5

どうやって高速化したの?

5 月の東京 RubyKaigi 11 で発表予定です。こうご期待。

今すぐ知りたい人はコード読めばいいと思います。

ref: http://github.com/mame/optcarrot/

余談

最適化っていうのは、「ここまで速くなればこれができる、速くならないとできない」というような、all or nothing な目標をもってやれば何とかなるものだなあと思いました。そのおかげで、まあ 3 fps から 60 fps までの 20 倍の高速化でも頑張れた。あと、コードの綺麗さと高速性の妥協点が決められる感じ。「少しでもいいから速くしたい」という煩悩は、漫然とコードを汚すばかり *6 で、最終的な効果が乏しいということになりがち。

*1:Topaz は最初 1 fps とかだったのですが、リファクタリングしたら 30 倍になりました。CPU のディスパッチを case 文から配列参照に置き換えたのが良かったみたい。JRuby や Rubinius も、たぶん同様にちょっとしたことで速くなるんだろうと思います。

*2:整数除算が整数になるようにするパッチを当ててます。

*3:GUI を表示する場合は、ruby-ffi 経由で SDL2 を使用します。

*4:Rubyボトルネックは昔と変わらずメソッド呼び出しのままなんだねーとか、インスタンス変数のアクセスは遅いなあとか。

*5:Topaz は生成コードが複雑すぎるせいか、コンパイルでエラーになるので動きませんでした。ただ、多分 Topaz も case 文が最適化されてないので、仮に --opt が動いても今すぐ MRI を超えることはないと思います。

*6:文字列リテラルに片っ端から .freeze つけるとかね。