0.128u4での変更点 by Aaron

0.128u4でCPUとメモリ周りの大幅な変更が行われましたが、その背景と未実装部分を含む変更点の具体的な解説がAaronさんのところに出ています。


CPUコンテキスト切り替え

MAMEが設計された当初、将来的に複数のCPUをエミュレートして同時使用することは想定されていませんでした。MAME v0.1は、Nicola氏によるパックマン系ハードをエミュレートする「Muilti-Pac」エミュレータが元になっています。これはZ80CPU1個だけで構成されています。似たようなハードウェアで動作するアーケードゲームが次第に見つかり、多くのZ80ベースのゲームがすぐにサポートされました。

もちろん、シングルZ80以外に様々な種類のアーケードハードがあることはすぐにわかりました。そこで、6502や6809などの8ビットCPUがサポートされることになります。さらに、多くのゲームが複数のCPUを使っていることも分かり、マルチCPU機能もサポートされました。問題というのは、CPUエミュレータ用のコードが、同じCPUの複数インスタンスを動作させるようには設計されていなかったということです。また、マルチCPUを動作させるために、"コンテキスト切り替え"というコンセプトが導入されたこともあります。

ここでは例としてZ80を採り上げます。2個以上のZ80CPUを同時使用する場合、Z80CPUエミュレータがCPUの状態をグローバル変数で保持することが主な問題点になります。当時(386やDOSの時代)には、Z80それぞれにメモリを割り当て、CPUの状態をポインタで参照するよりも、グローバル変数からデータを取得する方が高速だったのです。

この方法を使ってもZ80コアが動作し続けられるように導入されたのが、コンテキスト切り替えです。コンテキスト切り替えを使うと、MAMEは別のZ80を実行するために、一旦今のZ80の動作を止める必要がある場合、最初のZ80に関係するグローバル変数データを全て一時メモリにコピーします。次に、2番目のZ80の状態を一時メモリからグローバル変数へ戻します。この時点で、グローバル変数は2番目のZ80用になり、正しく実行できるようになるのです。

MAMEでは、最初のZ80を数千サイクル実行→コンテキスト切り替え→2番目のZ80を数千サイクル実行→コンテキスト切り替え…という動作をしていたので、実際にはこの方法で全く問題なく動作しました。コンテキストの切り替え自体は、エミュレーションサイクル数千回に1回しか行わないので、必要な時間はトータルの実行時間に比べてわずかだからです。

しかし、この2つのZ80を"サイクル単位で正確に"動作させたいケースでこれが問題となります。この場合、実際に1サイクルか2サイクル毎にそれぞれのZ80を動作させるため、切り替え動作が非常に多く起こります。最初のZ80を1、2サイクル→コンテキスト切り替え→2番目のZ80を1、2サイクル→また切り替え、という具合です。この状況では、CPU動作よりも切り替え動作の方に時間がかかる結果となりました。

この問題の解決方法は、コンテキスト切り替えを無くすことです。そのために、グローバル変数ではなく、各Z80割り当てたメモリから全てのデータを参照するようにしなくてはいけません。以前であれば、この方法ではパフォーマンスにある程度の影響が出ましたが、今のプロセッサとコンパイラを使えばその違いはほとんどありません。

というわけで、MAME 0.128u4での変更というは、このCPUのコンテキスト切り替えを一掃するというのが最大の動機になったのです。このために、全てのCPUコアへのインタフェースを変更し、各コアがグローバル変数ではなく、メモリポインタから状態を取得するように修正する必要があります。0.128u4の段階では、重要なCPUについてはコンバートが終わっていますが、その他についてはまだ作業中です。

メモリコンテキスト切り替え

CPUがコンテキスト切り替えを行う方法に加えて、MAMEのメモリシステムでも同様のことを行ってきました。これは、CPUがメモリの読み書きをするとき、メモリシステムに対して、どこに読み書き動作を行えばよいかを探し出します。あるCPUの実行動作をMAMEが切り替える場合は、新しいCPUが正しいメモリにアクセスできるよう、メモリコンテキストの切り替えも必要なのです。

メモリのコンテキスト切り替えを無くすには、メモリサブシステムの状態をグローバル変数から、割り当てたメモリへ移動する必要があります。しかし、メモリシステムがこの方法のために構成されていないため、大幅な変更が必要になりました。最終的に、メモリサブシステムの状態を表すため、"アドレス空間"のコンセプトを導入しています。

各CPUは1つ以上のアドレス空間を持つことができ、CPUとメモリシステムの通信が必要なときは、メモリシステムがメモリアクセスのマッピング方法がわかるように、メモリシステムに対し関連するアドレス空間へのポインタを渡します。さらに、ゲーム内でリード、ライト処理に使う個々のコールバックを特定するときにも、アドレス空間へのポインタを渡します。

これらがすべて動作するために、システム内のリードライトハンドラ用インターフェイスの更新、メモリシステムの書き直し、さらに全てのCPUコアでのこれらのアドレス空間オブジェクトも渡すような変更が必要となります。0.128u4の時点で、メモリコンテキスト切り替えは完全に取り除かれています。CPUのコンテキスト切り替えを無くす前に、この作業を終わらせることが不可欠なのです。

「アクティブ」なCPU

コンテキスト切り替えと同じく「アクティブ」CPUという考え方も今回の変更対象のひとつです。これは、グローバル変数に現在コピーされているCPUを定義するものです。CPUのコンテキスト切り替えを削除するにあたり、アクティブCPUというものも意味を持たなくなります。

例えば、CPU(ここではCPU Aとします)がもうひとつのCPU(CPU Bとします)のメモリ空間に書き込みができる場合を考えてみましょう。

コンテキスト切り替えシステムでは、CPU Aが動作中のとき、グローバル変数にはCPU Aの状態がロードされています。さらに、CPU Aのメモリリードライトは、メモリアクセス時の挙動を知るためにグローバルなメモリ状態情報を使います。論理的に、CPU AがアクティブなCPUとなります。では、CPU AがCPU Bのメモリ空間を更新するような動作をする場合はどうなるでしょう。このためには、まずCPU Aの状態(CPUとメモリの両方)をセーブして、CPU Bの状態をロードします。従って、CPU BがアクティブなCPUになります。次に、CPU Bのメモリ空間にアクセスします。これが終わったら、CPU Bの状態をセーブし、CPU Aの動作を続けるためにCPU Aの状態を戻します。

新システムでは、CPU Aが実行中なら、CPU Aの状態はメモリ空間内にあり、いつでもアクセスできます。アドレス空間も同じように設定され、コンテキスト切り替え無しでいつでもアクセス可能です。ここで、CPU Aが同様にCPU Bのメモリ関数を更新するとすれば、コンテキスト切り替えは一切必要ありません。代わりに、メモリ操作を行う際に、CPU Bのアドレス空間へのポインタを渡せばCPU Bのメモリを直接いじることができます。

これら2つには大きな違いがあるのは明白で、新システムの方が高速になるはずです。しかしそれよりも重要なのは、「アクティブ」なCPUというもの自体が存在しないということです。

CPU Aが実行中なのだから、それを「アクティブ」と指定するべきだという反論もあるでしょう。しかしもしそうだとすると、この例でならば以前とは異なりCPU Bがアクティブとはみなされません。さらに突き詰めると、もし実行中のCPUを「アクティブ」だと定義するなら、「アクティブ」な状態になれるCPUは1つだけですから、同時実行できるCPUは結局1つのみということになってしまいます。

したがって、「アクティブ」なCPUという概念自体を取り払うことが正しい解決法と言えます。残念なことに、MAMEのドライバやコアにはアクティブCPUを参照する箇所がかなりあるのですが、これらは徐々に取り除いていく必要があります。ひとつでも参照箇所があれば、コンテキスト切り替えを完全に削除することはできません。つまり、アクティブCPUへの参照が存在する限り、アクティブCPUというものの定義を有効にするため、コンテキスト切り替えをし続けなければならないということです。

結局のところは?

ユーザの視点からみると、既存のゲームは動作し続けますし、特にコンテキスト切り替えが激しく起きている状況では、動作速度も同等、あるいは少し向上するはずです。コンテキスト切り替えを全て削除してしまえば、CPU切り替えを頻繁に行う際にMAMEが実行することは減るはずです。

しかし長い期間でみれば、このようなケースでは再び速度が落ちるかもしれません。初期のゲームの多くでは、複数のCPUがとても密接な動作をしており、正確な動作を追及するならば、理想的には各CPUが命令を同時に実行することが望まれるため、切り替えを交互に素早く行う必要があります。現状のコンテキスト切り替えでもしこれを行うとパフォーマンスは大きく低下します。コンテキスト切り替えの削除が進めば、8-bitシステムなら最低限、許容範囲に収まればと考えています。

開発者にとっては、この変更でMAMEのオブジェクト指向がさらに強まることになります。MAMEはC++ではなく普通のC言語で書かれてはいますが、running_machine、device_config、address_spaceなど、システムのコアストラクチャはオブジェクトであり、ほとんどのシステム系コールではこれらへのポインタを渡しています。「アクティブ」なCPUが必要になるケースは今後ほとんど無いでしょうし、独自の関数へmachineへのポインタを渡す方向なので、グローバルなMachine(Mは大文字)への参照は削除します。

これらの変更は、ソフトウェア設計という視点から見れば、特に目新しいものではありません。しかし、MAMEの設計という観点では大きな革新と言えます。CPUコンテキスト切り替えの削除を進めるにあたって、その他の変更も多く行われる予定です。しかし、コアの基本構造に手を加えるためには、0.128u4の段階で適切だったと思われます。今回の変更自体でバグもいろいろ起きていますが、今後の更新は小規模なものになるはずです。

Aaron's Almanac

前の記事
次の記事