東京 Ruby 会議 11 直前特集号

Optcarrot: A pure-ruby NES emulator

Ruby の高速化を煽るためのベンチマークプログラムとして、任天堂の家庭用ゲーム機であるファミリーコンピュータのエミュレータ Optcarrot を Ruby で開発した。高速な Ruby プログラムを書くための工夫と、各種 Ruby 処理系のベンチマーク結果を紹介し、MRI の最適化方針について議論する。

必要となる知識

ハードウェアに関する基礎的な知識があると望ましい。

遠藤侑介

'Ruby コミッタ。かつてはテスト、コードカバレッジ、リリースマネジメントなどを担当していた。高速化にはあまり興味がない。また、この説明文のように役に立たないプログラムを書く「超絶技巧プログラミング」を提唱・実践している。'.tap{|s|printf(t=%{'%s'.tap{|s|printf(t=%%{%s},s,t)}},s,t)}


5/14 チャットにて伺いました(話し手:遠藤さん、聞き手:笹田)。

笹田 今回、NES(ファミコン)のエミュレータを ruby で書いていた、ということですね。まめめも に色々詳しく書かれていますが、暇だったんですか?

遠藤 はい。

笹田 仕様は公開されているんでしょうか。

遠藤 NesDev Wikiに、有志の解析結果が公開されています。NES エミュレータ開発者はみんなここを見ているようです。テスト用の ROM もここに公開されている親切設計。あとは、既存のエミュレータのソースコードを読むということですね。

笹田 なんでまた NES だったんでしょうか。

遠藤 スーパーファミコンは無理だと思ったからです。

笹田 ファミコンは簡単でした?

遠藤 いえ、これもあんまりできると思っていませんでした。

笹田 でも、出来ちゃったと。難しいのは性能的にでしょうか。それとも、仕様の大きさ的に?

遠藤 性能的にですね。

笹田 じゃあ、意外と Ruby でもなんとかなった、ということですね。ハードウェア全部エミュレーションしてるんでしょうか。

遠藤 もちろんです。256 × 240 の画面を 60 fps で更新しないといけなくて、60 フレーム分だけ配列に代入するループだけで 0.2 秒とかかかります。画面描画だけでそのくらいかかるわけです。60 fps でシステムを動かすためには、60 フレームの全処理を 1 秒で終える必要があるので、ピクセルの色の決定だの CPU エミュレーションだのを残りの 0.8 秒で行なわなければなりませんでした。

笹田 無理ゲーじゃないですか。

遠藤 そういう無理ゲーでした

笹田 ゲーム動きました?

遠藤 はい。市販ゲームはスーパーマリオブラザーズしか試してないですが。マリオはわざわざ秋葉原で買ってきました。吸出し機も自作して。こーどねーむ「ホンコン」 with Arduino をマネただけですけどね。

笹田 どの程度のクオリティで動いたんでしょうか

遠藤 普通に遊んでで違和感はないです。明らかにピクセルが化けてるとかそういうのはありません。また、明らかな遅延を感じるとかもないですね。

笹田 凄いですね。

遠藤 Ruby すごいですね。

笹田 今回の発表は、そのための最適化技法って感じですか。

遠藤 はい、如何に実装したかですね。

笹田 どういうプラットフォームで動きますか?

遠藤 SDL2 が動けばなんでも。Windows でも、mingw なら動いてますね。mswin だと動かないという噂もあります。

笹田 最適化の話というと、こんな感じですか。

  1. 普通に書けば良かった
  2. 工夫したらいけた
  3. 超絶技巧(最適化)でいけた
  4. どうやっても駄目だった

遠藤 はい。比較的普通な Ruby コードで 20 fps まで頑張って、そこからコードの綺麗さを捨てて頑張って 60 fps を目指したというところですね。

笹田 1. ~ 3. の話ですね。

遠藤 あと、これはやっていないことの話なのですが、NES エミュレータっていうのは、簡単な ROM を動かすところまではわりと簡単なんですが、難しい ROM を動かすためにはいろいろやらないとダメなんですよね。NES のカートリッジって単なるソフトウェアじゃなくて、そのゲーム専用の回路とか入ってるんです。エミュレータは個々のゲームの回路も実装しないといけない。なので、ベーシックな機能の ROM しか動かない状態です。マリオ 3 とかで使われてる回路(MMC3)のエミュレーションはまだまだ遅いです。特定の信号線が立ち上がるタイミングをトリガーに動いたりするので、エミュレーションでそのへんの機能を抽象化出来ないから、ハードウェアを真面目にエミュレーションしないといけない。

笹田 行なった最適化を紹介してもらっていいですか。

遠藤 日記に書いてある部分は、20 fps を 60 fps にするためにやったことですね。

具体的には、まず自分自身のソースコードを読み込んで、メソッドインライン展開したり、簡単な部分計算したり、fastpath をこしらえたりして、コードクローンだらけの高速だけど最悪なコードを内部的に生成します。この処理は Ruby の得意とする文字列処理なんで、正規表現を駆使して適当にやってます。で、生成されたプログラム文字列を eval することでボトルネックの処理を置き換えます。

20 fps 達成までにも、そこそこアルゴリズムレベルの最適化をやっています。

笹田 というと?

遠藤 一番最初、本当にナイーブに書いたら 3 fps とかだったんですよ。それを 20 fps にした、約 7 倍。図解しないとわかりにくいんですが、実機だと CPU と GPU の 2 つが同時に動作するわけですが、エミュレーションでは並列実行できないので、ナイーブな実装だと CPU を 1 クロック分すすめて、それから GPU を対応するクロック分進める、というのを繰り返してとても遅いです。loop { @cpu.step; @gpu.step } みたいな感じで、メソッド呼び出しが多すぎる。そこで、まず CPU が GPU と通信するまで CPU だけエミュレーションして、通信したらそこで止めて、CPU に追いつくまで GPU をエミュレーションする、ということを行ないました。実際には双方向なのでもうちょっと難しいですが。ちなみにこの方法は自分のオリジナルの方法じゃなくて、NES 業界ではわりと一般的な方法(キャッチアップ法)です。

笹田 なるほど。主にメソッド呼び出しが重いですか。

遠藤 やっぱりメソッド呼び出しが遅いですねえ

笹田 ハードウェアエミュレーションだと、1 メソッドあたりの実体が短いので、それを感じやすい、って話な気もしますが。

遠藤 かもしれません。で、このキャッチアップ法でだいたい 10 fps になって、GPUのエミュレーションをアルゴリズムレベルで大きく工夫したあと(詳しくは発表で)、細かい最適化を積み重ねて 20 fps にしました。

笹田 細かい最適化。

遠藤 ビット演算がわりと遅いんですよね。これを配列参照に置き換えました。0x23C0 | (addr & 0x0C00) | (addr >> 4 & 0x0038) | (addr >> 2 & 0x0007) こういうのとかを、LUT[addr] に置き換えてしまう。流石に 1 個のビット演算を置き換えるわけではありません。

笹田 なるほど。

遠藤 こんなコードを綺麗な Ruby コードと言うかどうかは微妙だけども。あとはオブジェクトが生成されないようにするとか。GC 以前にオブジェクト生成自体が遅いので。初期化が終わったら 1 つもオブジェクトを作らないようにしました。MMC3 は除いて。

笹田 なんか組込みみたいな話になってきましたね。

遠藤 malloc 禁止みたいな。NES エミュレータに関して言えば、オブジェクトを作らないようにコードを書くのは大して難しくなかったです。文字列使わないですからね。

笹田 なるほど。

遠藤 そうそう、メモリ表現するのに文字列使いたいと思うかもだけど、整数の配列に比べて全然遅いので話にならない。

笹田 途中書き換えが?

遠藤 あんまりちゃんと調べてないけど、読み出しも書き出しも遅そうな感じでした。

笹田 配列偉い。という、NES を作る上でのパフォーマンスチューニング技法について、ご発表頂ける感じでしょうか

遠藤 というのが半分と、後は 20 fps を 60 fps にする時にやったコード書き換え実験ですね。こちらは、一般の Ruby ユーザがやるべきとは思わないですけどね。メソッド定義とかしたくなくなるので心の弱い人は見てはいけない。普通は読みやすいコードを書くべきだと思います。

笹田 聞く人が予習しておくべき話はなんでしょうね。日記読んでおけ、とかでしょうか。

遠藤 クロックって何?っていう人には前半はよくわからないかもしれない

笹田 「ハードウェアに関する基礎的な知識があると望ましい。」って発表概要にも書いてありましたね。

遠藤 まあ、そこまで知らなくても大丈夫な気はしますが。

笹田 どれくらいまでを基礎というかは、遠藤さんのレベルを考えると難しい気がしますが。

遠藤 あと CPU は意外に話題に出てこない。NES エミュレーションのボトルネックは GPU なんです。エミュレーション実行時間の 80 % は GPU。

笹田 そもそも、GPU が乗っていることすら知りませんでした。

遠藤 ゲーム専用 GPU を載せたことが、ファミコンが勝利した主因ですね。

笹田 ちなみに、音の制御は?

遠藤 44100 Hz とかだと、原理的には 1 秒間に 44100 回の処理でいいわけなので、全然処理時間はかからないです。

笹田 難しくなかった、と。

遠藤 実装するアイテムは多いのでそういう意味では難しいけど、最適化には全然関わってないです。

笹田 音は、おかしいとすぐにわかるし、タイミングに対して凄くシビアだと思うんだけど、そういう同期を取るのも難しくない、と。

遠藤 何も考えずにフレームごとにフラッシュしてるだけですね。多少、SDL2 のサウンドバッファで遅延とかするんだろうけど、自分の感覚では違和感ないです。

笹田 60 fps 出ればよい、と。60 fps 超えてもだめですよね、その辺のタイミング制御は何でやってるんですか?

遠藤 SDL_GetTicks()SDL_Delay() でやっています。普通にゲーム作る感じです。

笹田 なるほど、ありがとうございました。遠藤さんの超絶技巧の新機軸、楽しみにしています。

遠藤 わりとすべて話したのでもう発表聞かなくても行けるな。

笹田 あと、本が展示されるんですよね。

遠藤 発表には関係ないけど『あなたの知らない超絶技巧プログラミングの世界』を東京 Ruby 会議 11 の会場で展示予定です。

笹田 当日、手にとって、読んでみることができると思うので、気に入ったら、その辺の本屋で買ってきて、遠藤さんにサインを貰えるということですね。本日は、ありがとうございました。