TOP
MAP
UP
エミュレータ(Hornet)におけるAMD64の最適化等について
おおもとのレポートではhornet等についての解説があるが、ここでは割愛(hornetの所を参照)
以前よりAZUCO製作のエミュはSwitchによるデコードを行ってきた。しかしアセンブラ使いとしてはTable jumpが速いのではないか?という疑問は常にあり、以前にSH2のエミュレーションコアにおいて通常のSwitchデコードと(その時はアセンブラによる)Tableデコードを比較してみた。その時は486の時代であったが、結果はTableデコードの方が遅い、という衝撃の結果であった。
それとは別の話であるが、アセンブラの手動による最適化に限界を感じた自分は(単純に、非常に生産性・保守性が悪い)コンパイラの最適化に期待すべく、Switchデコードにおいて#includeによる各命令のコードを挿入するという方法を取っていた(これはコンパイラによる大域的な最適化を期待したのと、関数呼び出しのオーバーヘッドを嫌った為である。また#includeによりソースの見通しはそんなに悪くない)しかしMC68Kといった複雑なCISCプロセッサのエミュレーションにおいて、そういった大域的な最適化がそもそも行われるのかはかなり疑問に思い始めた(実際ほとんど最適化されないと思われる)
そういった疑問から、コードの局所性を高める事で、レジスタ割り当てや最適化が期待できるのではないか?という思いから命令の関数化をテストする必要性があると感じた。
また著名なエミュレータであるMAME等はListによるデコードを行っていたので(現在変わっている可能性あり。ダイナミックリコンパイル併用もあり)そもそも関数ポインタを使う事によるパイプラインフォールトもそんなに気にするほどのコストがかからないのではないか?という疑問からTableを再度テストする必要があると感じていた。
といった事から、命令の関数化、デコードのTable化の検討を行うこととした。
検討ケース
ケース | デコード方法 | 命令の関数化 | 備考 |
C1 | Switch | なし | |
C2 | Table | あり | |
C3 | Switch | あり | |
測定結果
環境 | ビルド | C1 | C2 | C3 |
P4 2.8GHz HTT | debug | 0.4097 | 1.0504 | 1.0280 |
P4 2.8GHz HTT | release | 7.5819 | 8.3966 | 7.6597 |
P4 2.8GHz | debug | 0.4123 | 0.8635 | 0.9110 |
P4 2.8GHz | release | 6.8146 | 7.0638 | 6.7671 |
Athlon64 3200+ 32bit | debug | 0.7220 | 1.2468 | 1.2158 |
Athlon64 3200+ 32bit | release | 7.0489 | 6.2295 | 6.3022 |
Athlon64 3200+ 64bit | debug | 0.5158 | 0.8234 | 0.7694 |
Athlon64 3200+ 64bit | release | 8.5881 | 8.8298 | 8.4652 |
Opteron246 3200+ 32bit | debug | 0.7262 | 1.2281 | 1.1896 |
Opteron246 3200+ 32bit | release | 6.7270 | 6.1834 | 6.4149 |
Opteron246 3200+ 64bit | debug | 0.8417 | 1.4516 | 1.3475 |
Opteron246 3200+ 64bit | release | 8.1434 | 8.5922 | 9.3692 |
数値の単位は1命令=1clock換算でMHzで表記
HTTとはHyper Threading Technologyの事
64bitとはWindows XP x64editionにおいて、AMD64でコンパイルしたHornetで計測したもの(つまり64bitネイティブ)
Debug/Releaseは単にコンパイルオプションの最適化ある/なしの違いではなく、ディレクティブの違いによりデバックコードのあり/なしでコード量が違うので、単純に最適化具合を見れるという話にはならないので注意。
全ケースのソースコードはこちら
C1は従来のデコード方式である(ただし測定方法のみ今回の新しい方法に変更して揃えてある)
次に作成したC2はTableと関数化を導入したものである。この時の想定ではC2がC1よりも速度が稼げる、という予測でこのケースを実装した(なお、多段Tableによるデコードを防ぐために、デコードの初期で多段デコードになるかどうかをSwitchで検出し、テーブルを引くのは一回だけになるようにしてある)
しかし実際にはAthlon64/Opteron246の32bitにおいて逆にパフォーマンスが低下するという結果であったので、低下する原因がTableなのか関数化なのかを調査する為にC3を作成する事となった。
ちなみに関数化される命令はvoid ops(void)という型であるので、パラメータが付随しない為(パラメータをレジスタ渡しにする)64bitモードで特に大きなアドバンテージがあるわけではない。
関数化によるメリットは、関数化による局所性の増加に伴うコードの最適化(特にレジスタ割付の増大)であり、デメリットは(パラメータプッシュの無い・・・・と言っても実際にはclassのメンバ関数であるのでthisはプッシュするが)純粋な関数呼び出しのオーバーヘッドのみである。
今回以前に実装していた速度の計測方法は、Virtual CPUとは別のスレッドで、Virtual CPU内で1命令実行毎にカウントアップしていくカウンタをまず参照し、その後Win32APIのSleep(2000)を実行(2000msecのウェイトがスレッドにかかる)ウェイト復帰後、再度カウンタを参照し、実際にいくつの命令を参照出来たかを計算し、秒間あたりの実行命令数(1clock=1命令という仮定)からクロック数を割り出していた。
この計測方法の問題点は、そもそもSleep自体が非常に精度の悪い(特に過剰な負荷がかかっている場合において、printf等のある特定の関数が実行されない限り、ウェイト解除がかなり遅れる)タイマである事だ。
また余談であるが、そもそもWindows自体がRTOS(Real Time OS)としては基本的に期待できないので(一部のジェット機はサードパーティーのRTOS化ソフトの入ったWindowsNTで動いている)セットしたタイミングで確実に制御が戻ってくる事は期待できない(特に今回のエミュレータのような、高負荷のマルチスレッドのソフトでは)
以上の事から今回は、CPUのデコードルーチン内で一定回数のデコード毎にパフォーマンスカウンタ(Win32API QueryPerformanceCounter)を読み出し、そこから一定時間にどれくらいのデコードを行ったかを逆算する、という計測方法に変更した。
ただしこの計測方法も、最近のCPUに実装されている動的クロック変化機能があった場合に正常な計測が出来ないので、計測時にはOFFにしなければならない。
考察についてはレポート中にはいろいろ書いたのだが、Athlon/Opteron 32bitでのパフォーマンスが悪いという部分がどうにも引っかかっており(であるのでレポート中ではこの部分はあえて除外した:その時のタイミングではOpteronも悪いというのは判明してなかったので)総論的な結論というのはちゃんとした物を出せていない。
であるが、大体おおまかな結果として、命令の関数化(コードの局所性の増加)でコードの速度を上げる事が出来る、table jampは(tableが十分に広い場合等)条件が揃えば十分に速度を稼げる、64bit化されたソフトは大幅にパフォーマンスアップする、という事はある程度言えるのではないかと思う。
Athlon/Opteron 32bitにおいて結果が想定と異なる部分については、レポート中でも予告したが、今後に可能な限り追求してみたいと思う(でないとどういう方針でコードを組むかすら決まらんので:汗)という事で、このページは続く・・・