2013-11-02

DOOM 3 BFGの技術ノート

Doom3 BFG Documentation
Doom 3 BFG Technical Note

DOOM 3 BFGとは、2004年に発売された不自由なWindows用の、プログラムだけは自由なゲーム、DOOM 3を、最新のハードウェア用に移植したものである。id SoftwareのJ.M.P. van Waverenによって書かれた、移植のときの技術ノートが、とても興味深い。

まず、DOOM 3 BFGの移植は、結構難しかったらしい。というのも、オリジナルのDOOM 3は、2004年当時のローエンドからミドルエンドのハードウェアで、640x480で20fpsを出せるぐらいだった。これを、2012年のハードウェアで動くWindows、XBox360、PS3で、1280x720で安定して60FPSを出せるようにしなければならない。

DOOM 3は、当時としてはクソ重かった。技術的には野心的な作品だが、野心的すぎたために、見た目にしてはクソ重いという、アクション性が重要視されるゲームとしては最悪の出来だった。それに、ゲーム自体もあまり面白くなかった。私は、当時同時期に発売されたPainkillerこそが正統なDOOMとDOOM 2の後継であると思う。

それはともかく、解像度とFPSの差を考えると、DOOM 3 BFGは、現在のハードウェアで、当時の10倍速く走らなければならないということだ。

現在のハードウェアは、確かに当時より性能が上がっているが、その上がりぐらいは、バランスが良くない。

たとえばCPUは、サイクルあたりの命令実行数が上がった。これにより、性能は上がっているのだが、当時と比べて驚くほど上がっているわけではない。サイクル数はほとんど上がっていないからだ。もし、当時と同じペースでサイクル数が上がり続けていたら、今頃は近所のPCショップで10GHz超えのCPUがお手軽価格で買えたはずだ。

かわりに、最近のCPUはマルチコアになっている。ただし、これはプログラムを並列実行できるように書きなおさなければならない。オジリナルのDOOM 3は当時のハードウェアにあわせて、性能を必要とする処理はすべて単一のスレッドで実行されている。

一方、GPUの機能と性能は驚くほど向上した。もともとが極端な並列実行だったので、自然な発展だ。

まず問題になったのが、メモリだという。メモリ帯域はあまり向上していない。CPUやGPUの性能向上ほどメモリの性能は上がっていない。

例えばスキンメッシュだ。3Dモデルのアニメーションに合わせて大量の頂点、すなわち大量の浮動小数点数を、破綻しないように変更しなければならない。DOOM 3では、スキンメッシュはCPUで1フレームに一回だけ計算して、メモリに格納し、スキンメッシュが必要な場所で使っていたという。

ところが、メモリの性能がそれほど上がっていないために、これは現代のハードウェアでは、とても遅くなる。メモリの読み書きが発生すると、それだけでメモリ帯域を食うし、メモリキャッシュをふっとばすし、CPUとGPUの間のデータ転送の帯域も食う。

そこで、DOOM 3 BFGでは、計算済みのスキンメッシュをキャッシュせず、CPUだろうとGPUだろうと、スキンメッシュが必要なその場で計算をするのだという。現代のハードウェアでは、そもそもスキンメッシュをストリーミングする事自体にコストがかかり、ストリーミングしながら計算したところで、ストリーミングより遅くなることはないのだという。つまり、計算は無料なのだ。

これにより計算結果をメモリに書きだす必要もなくなり、貴重なメモリ帯域を節約できる。また、スキンメッシュが計算済みかどうかなどの「状態」をなくすことができ、管理する状態(state)を削減することに成功した。状態を減らす(stateless)ことは、並列処理にも都合がいい。

たとえば、DOOM 3 BFGでは至って普通のシーンである、影を落とす物体に影を落とす光源が二つあるといったようなシーンでは、1フレームあたり、CPUとGPUを合わせて、7回のスキンメッシュの計算を行わなければならない。DOOM 3では一回だけ計算してキャッシュしていたのにくらべると、実に7倍の計算量であり、当然、7倍のFLOPSが必要になるが、現代のハードウェアでは、むしろ速くなるのだという。

その他に変更したのは、データ構造だ。たとえば、DOOM 3では、独自実装の双方向リストを使っていた。

template< class type >
class idLinkList {
    idLinkList * head;
    idLinkList * next;
    idLinkList * prev;
    type * owner;
};

まあ、よくある形だ。これは、型の特殊化ごとにコードを生成してふくれ上がるという問題もあるがもっとも大きな問題は、双方向リストを管理するためのidlinkList<T>のメモリと、要素のtypeのメモリを、別々に確保することだ。

これは、キャッシュの都合上、とてもよろしくない。

DOOM 3 BFGでは、intrusiveな実装に書き換えたという。そのような実装では、typeにあたる要素の型はこういう形になる。

class idMyClass {
    bool valid;
    byte otherMembers[64];
    idMyClass * next;
};

このidMyClassのような型のオブジェクトは、配列としてメモリ上で連続した領域に確保される。idListは、その連続したメモリ上に確保されたオブジェクトへのインデックスを保持するようにした。これにより、要素の追加削除のたびにメモリの確保や解放がなくなり、またメモリが一箇所に集められることにより、キャッシュに乗りやすくなったという。

また、DOOM 3では、idHashTableもよく用いられていたが、この実装も、やはり管理するためのメモリと、要素のためのメモリは別々に確保されていたので、パフォーマンス上の問題があった。多くのハッシュテーブルの利用は、単にリストを使ってもパフォーマンス的に問題がないので、リストを使うようにしたという。また、どうしてもハッシュテーブルのパフォーマンス特性が欲しい場合でも、やはり連続したメモリ領域に確保された要素オブジェクトへのインデックスを保持するintrusiveな実装のidHashIndexを使ったという。

技術ノートでは結論として、DOOM 3を再び最適化することにより、現在のハードウェアの制約を見出したとしている。また、メモリキャッシュを意識することがとても重要だとしている。

この技術ノートを読んで、もはや現代のコンピューターの性能を、単に整数や浮動小数点数の演算能力で測るのは、もはや正しくないのではないかと思った。メモリ帯域とかストレージ帯域、あるいはネットワーク帯域などで測ったほうが、むしろいいのではないかという感想も持った。

3 comments:

Anonymous said...

10GHzのCPU = 電子レンジ

Anonymous said...

要するにボトルネックが移動したって話ですね

Anonymous said...

電子レンジは違うだろ