サンプル3. DirectXランタイムのProcessor Specific Graphics Pipeline(PSGP)を無効化する
コアランタイムおよびD3DXの内部計算は、さまざまなCPUで高速に実行されるようにDirectXチームの手で高度に最適化されています。開発者はD3DXとリンクするだけで、最新のCPUに対応した命令を自動的に利用できます。
この機能はProcessor Specific Graphics Pipeline(PSGP)と呼ばれることがありあます。
この仕組みは確かにスピードの面では有用なのですが、計算の再現性もまた大事な要素であることを忘れてはいけません。浮動小数点演算の結果の微妙な違いですら、ゲームリプレイやネットワークゲーム共有世界に致命的な綻びを作り出すには十分です。
DirectXを使用したアプリケーションで、CPUによって浮動小数点数演算の結果が異なることで困った場合は、PSGPを無効化することで問題を解決できる可能性があります。PSGPを無効化するにはレジストリの次に示す位置にDWORD型の値を作成し1を設定します。これにより、DirectXの計算全てがx87 FPUによって行われるようになります。
- 「HKLM\Software\Microsoft\Direct3D\DisablePSGP」
- 「HKLM\Software\Microsoft\Direct3D\DisableD3DXPSGP」
筆者が試した限り、D3DXはDLLがロードされたときではなく、PSGPが関係する関数が始めて使用されたときにこれらのレジストリをチェックするようです。フックハンドラによる制御自体は、これまでのサンプルコードを流用すれば簡単に実装できるでしょう。
手元の環境で計算結果が異なるいくつかのテストコードをまとめたものが「Sample3」です。無事にフックが成功すれば、起動時にPSGPを無効化するかどうか聞いてくるはずですので、皆さんの環境では結果がどうなるか確かめて見てください。参考までに筆者がテストした環境での結果を表5に示します。
| 構成番号 | case 1 | case 2 | case 3 |
| 1 | 2.22045e-016 | 1.#INF | 1.70141e+038 |
| 2 | 0 | 1.#INF | 1.#INF |
| 3 | 2.22045e-016 | 1.#INF | 1.70141e+038 |
| 4 | 0 | 1.#INF | 1.#INF |
| 5 | 2.22045e-016 | 1.#INF | 1.70141e+038 |
| 6 | 0 | 3.40282e+038 | 1.70141e+038 |
| 7 | 0 | 1.#INF | 1.#INF |
| 8 | 0 | 1.#INF | 1.#INF |
テスト環境の構成
- Windows XP Professional SP2, Intel XEON(Prestonia) 2.0GHz, PSGP off
- Windows XP Professional SP2, Intel XEON(Prestonia) 2.0GHz, PSGP on
- Windows XP Professional SP2, Pentium III-M 800MHz, PSGP off
- Windows XP Professional SP2, Pentium III-M 800MHz, PSGP on
- Windows XP Professional x64Edition, AMD Athlon 64 3500+, 32bit mode, PSGP off
- Windows XP Professional x64Edition, AMD Athlon 64 3500+, 32bit mode, PSGP on
- Windows XP Professional x64Edition, AMD Athlon 64 3500+, 64bit mode, PSGP off
- Windows XP Professional x64Edition, AMD Athlon 64 3500+, 64bit mode, PSGP on
D3DCREATE_FPU_PRESERVEを指定すると、DirectX Graphicsは(少なくとも見かけ上は)丸め精度に変更が行われなかったかのように振舞います。x87 FPUには、レジスタに格納される浮動小数点数を常に80bitの拡張精度で保持するという特徴があります(参考資料18)。他の多くのIEEE 754実装が、精度ごとに異なる演算命令を用意しているのに対し、x87 FPUはコントロールレジスタに丸め精度についての制御ビットを持たせることで、同じ命令で異なる丸め精度の計算を行うようになっています。つまり、C言語での
float型の変数を使用しても倍精度での計算が行われることもありますし、double型の変数を使用しても単精度の結果しか得られないこともあるということです。x87 FPUでは実メモリ上での格納形式が単精度(float)であるか倍精度(double)であるかということと、浮動小数点レジスタ上での演算精度が単精度であるか倍精度であるかということは必ずしも一致しません。doubleと宣言すれば常に高精度の演算が行われているという考えはx87 FPU環境でよくある誤りです。実際先ほど述べたDirectX Graphicsのデフォルト動作は、それ以降に新しい丸め精度が設定されるまでの間、全ての演算精度を単精度相当に丸めてしてしまいます。しかし本当の問題は、x87 FPUは丸め精度の設定を仮数部にのみに適用し、指数部は常に15bit精度として扱うということです。x87 FPUのこの振る舞いは確かにIEEE 754の要請以上の高精度な計算を可能にしますが、他の多くのIEEE 754実装と計算結果が異なる原因にもなっています。x87 FPU以外のアーキテクチャでは、演算途中の数値について指数部も含めて指定された丸め精度を使用するのが一般的です。
この違いにより具体的には、他のアーキテクチャでは指数部が桁あふれを起こす計算が、x87 FPUでは桁あふれを起こさないという事態が発生します。またx87 FPUであっても、レジスタからメモリに退避させるときに単精度(32bit)または倍精度(64bit)として書き出すと、その時点で必要に応じて指数部に丸めが発生します。これはつまり全ての演算がレジスタ上で完了する場合と、計算の途中結果を単精度または倍精度として何度かメモリに退避した場合とで、演算結果が異なりうるということを意味します。さらに特殊な状況として二重丸めを考慮する必要もあります(参考資料18)。
計算の再現性を重視したJavaは、厳密な浮動小数点演算のセマンティクス(FP-strict)が定められています(参考資料11)。Java 2以降では、
strictfpキーワードが指定された箇所において、このセマンティクスに従うことが要請されています。FP-strictは計算の中間値について、x87 FPUのような振る舞いではなく、他の一般的なアーキテクチャが行うように指数部についても指定された精度での演算を要請します。x87 FPUでこのFP-strictの要請を満たそうとすると、比較的大きなオーバーヘッドを伴うことが知られています(参考資料12、参考資料18)。Java 2がstrictfpキーワードを定義し、全ての計算についてFP-strictを要求することを諦めた背景にはこのような事情があると考えられます。さてVisual C++ 2005コンパイラにも、浮動小数点演算のセマンティクスを指定するコンパイルスイッチが存在します(参考資料13)。残念ながらVisual C++ 2005にはx86、IA64、x64-64全てのプラットフォームで同一のセマンティクスを使用する設定はありません。具体的には丸めモードのセマンティクスがプラットフォームに依存することを回避できないのです(参考資料14)。
このうちx64-64環境での
fp:preciseとfp:strictは、丸め精度について、JavaのFP-strictと同じセマンティクスが使用されます。このことは次のテストコードで確かめられます。#pragma float_control( except, on ) #pragma float_control( precise, on ) #pragma fp_contract( off ) #pragma fenv_access( on ) #include <cfloat> #include <cmath> #include <iostream> void Test1(); void Test2(); int main() { Test1(); Test2(); return 0; } void Test1() { const __int64 l1 = 0x7fe0000000000000; double d1 = * reinterpret_cast<const double*>( &l1 ); double d2 = * reinterpret_cast<const double*>( &l1 ); d1 += d2; d1 -= d2; if( _fpclass( d1 ) != _FPCLASS_PINF ) { std::cout << "[a] FP-strict violation" << std::endl; } } void Test2() { const __int64 l1 = 0x0008008000000000; const __int64 l2 = 0x3ff0000000000001; const __int64 l3 = 0x000fffffffffffff; const __int64 l4 = 0x3fefffffffffffff; const double d1 = * reinterpret_cast<const double*>( &l1 ); const double d2 = * reinterpret_cast<const double*>( &l2 ); const double d3 = * reinterpret_cast<const double*>( &l3 ); const double d4 = * reinterpret_cast<const double*>( &l4 ); const double ret1 = d1 * d2; const double ret2 = d3 / d4; __int64 dr1 = * reinterpret_cast<const __int64*>( &ret1 ); __int64 dr2 = * reinterpret_cast<const __int64*>( &ret2 ); if( ( dr1 & 0x1 ) != 1 ) { std::cout << "[b] FP-strict violation" << std::endl; } if( ( dr2 & 0x1 ) != 1 ) { std::cout << "[c] FP-strict violation" << std::endl; } }
ただしx64 EditionのWindowsで、32bitアプリケーションの互換性が失われるわけではありません。Legacy Modeでのx87 FPUやMMXの使用は引き続きサポートされています。
実はSSE/SSE2には、他のアーキテクチャ同様にFP-strictを満たすという特徴があります。これはVisual C++ 2005がx86-64アーキテクチャで使用する丸めモードのセマンティクスと首尾一貫しています。x64 EditionのWindows環境は、FP-strictが満たされる処理系と非常に相性が良いと言えます。
C99ではFLT_EVAL_METHODマクロが導入され、浮動小数点演算のセマンティクスを意識したプログラミングを行う下地が整いつつあります(参考資料16)。一方でDirect3D 10に対応するハードウェアは、IEEE 754サポートを義務付けられると言われており(参考資料17)、本格的なGPGPU時代の幕開けが予想されます。再現可能な計算の実現は、今後ますます重要な意味を持つようになることでしょう。
まとめ
今回は、APIフックによって擬似的なレジストリ設定を行うというテクニックが、DirectXアプリケーションの開発に役立つ場面をいくつか紹介しました。今回のテクニックとアイディア自体はありふれたものですので、応用の足がかりになる事柄をなるべく多く取り上げたつもりです。今回はたまたまDirectXという題材を使用しましたが、色々つまみ食いしていただき、皆さんの手でさまざまな場面に合わせて料理していただければ幸いです。
参考資料
- 『インサイド Microsoft Windows 第4版 上』 David Solomon・Mark Russinovich 著、豊田孝 訳、日経BPソフトプレス、2005年8月
- @IT 『次世代開発基盤技術“Software Factories”詳解』 萩原正義 著
- DirectX SDK Reference 『D3DXCreateTextureFromFile』
- DirectX SDK Reference 『Effect Format』
- 『Advanced Windows 改訂第4版』 Jeffrey Richter 著、(株)ロングテール・長尾高弘 訳、アスキー、2001年5月
- 『.NET & Windows プログラマのためのデバッグテクニック徹底解説』 John Robbins 著、豊田孝 訳、日経BPソフトプレス、2003年10月
- MSDN Library 『x64 Software Conventions』
- mgrier's WebLog 『How the NT Loader works』
- mgrier's WebLog 『The NT DLL Loader: reentrancy - play along at home!』
- mgrier's WebLog 『The NT DLL Loader: FreeLibrary()』
- 『Numerical Computing in Java ― 演算結果の再現性と性能』 首藤一幸 著、2002年12月
- 『厳密な浮動小数点演算セマンティクスのJava実行時コンパイラへの実装』 首藤一幸・関口智嗣・村岡洋一 著
- Visual C++ Compiler Options 『/fp (Specify Floating-Point Behavior)』
- MSDN Library 『Microsoft Visual C++ Floating-Point Optimization』 Eric Fleegal 著、2004年6月
- WHDC 『Mike Wall. Tricks for Porting Applications to 64-Bit Windows on AMD64 Architecture』 Mike Wall 著、2004年2月
- C99 5.2.4.2.2 Characteristics of floating types <float.h>
- 『Windows Graphics Foundation (In WINHEC 2004)』 David Blythe 著、2004年5月
- AP-943 インテル アーキテクチャ (IA) 浮動小数点ユニット (FPU)、ストリーミング SIMD 拡張命令 (SSE)、ストリーミング SIMD 拡張命令2 (SSE2) を使用した浮動小数点算術演算
- PECompact v2.x Document : TLS Callback
