x87 FPU で厳密な単精度演算を行うコスト
senna_hppの日記より.
DirectXから入ったので、浮動小数点演算にはfloat型演算を使うようになったのですが、実際にはdouble型の方が速いそうなのでその確認のための検証実験。
(中略)
type add(ms) sub(ms) multi(ms) div(ms) double 203.6 201.9 5.6 5.4 flaot 913.4 912.9 674.0 675.1 double(template) 201.1 201.3 5.1 5.4 float (template) 913.8 915.0 680.2 675.5 double(template specialization) 201.7 201.5 6.5 4.7 float(template specialization) 913.6 912.2 674.0 674.2 結果としてはdouble型が完全勝利と言えるでしょう。
ただし、PS2などはfloat型演算の方が速いので、処理系やコンパイラによって常にこのような結果になるとは限りません。
試しに手元の Visual Studio 2005 でもコンパイルさせてみましたが,ディスアセンブル結果を見てると,何となく状況が読めてきたり.Release ビルドのデフォルト値から想像するに,恐らくコンパイルオプションの「浮動小数点モデル」が「Precise (/fp:precise)」になっているのかと思います.
// double 版 (速い) for(int i=0;i<N;++i) a1 += a2; 00401069 sub eax,1 0040106C fadd st(3),st 0040106E fadd st(2),st 00401070 fadd st(1),st 00401072 fadd st(3),st 00401074 fadd st(2),st 00401076 fadd st(1),st 00401078 fadd st(3),st 0040107A fadd st(2),st 0040107C fadd st(1),st 0040107E fadd st(3),st 00401080 fadd st(2),st 00401082 fadd st(1),st // float 版 (遅い) for(int i=0;i<N;++i) a1 += a2; 004013AD fld dword ptr [esp+20h] 004013B1 fadd st,st(1) 004013B3 fstp dword ptr [esp+20h] 004013B7 fld dword ptr [esp+24h] 004013BB fadd st,st(1) 004013BD fstp dword ptr [esp+24h] 004013C1 fld dword ptr [esp+28h] 004013C5 fadd st,st(1) 004013C7 fstp dword ptr [esp+28h] 004013CB fld dword ptr [esp+20h] 004013CF fadd st,st(1) 004013D1 fstp dword ptr [esp+20h] 004013D5 fld dword ptr [esp+24h] 004013D9 fadd st,st(1) 004013DB fstp dword ptr [esp+24h] 004013DF fld dword ptr [esp+28h]
詳細については以前の日記や例の記事で書いたので省略しますが,問題の float 版では,連続して fadd していくと指数部が 80-bit 精度で計算され続けてしまうため,毎回メモリに書き出して読み直すことで単精度での完全丸めを行っているようです*1.
見るからに悲惨ですが,こうやってパフォーマンス差と一緒に見せられると確かにインパクトありますねぇ.
ちなみにこのテストコードで「浮動小数点モデル」を「Fast (/fp:fast)」にすると,概ね float の方が速いという結果になるようです*2.
CodeZine の記事用に作ったけど結局使わなかった図もついでに貼ってみたり.
*1:store-reload で FP-strict な単精度演算が実現できることについては,首藤氏の『厳密な浮動小数点演算セマンティクスの Java 実行時コンパイラへの実装』参照のこと
*2:追記.とはいえほとんど差はない
x87 FPU で厳密な単精度演算を行うコスト (2)
そうそう書き忘れてましたが, add や sub に比べて一部の multi や div が数十倍速くなっているのは,実際にはコンパイル時に浮動小数点数のループ計算が行われていて,その結果が直接埋め込まれるからです.実行時に計算しているわけじゃないので速度比較では注意しないといけませんね.
/fp:strictの結果
type add(ms) sub(ms) multi(ms) div(ms) 計算結果 double 962.3 962.8 21.7 20.4 0 flaot 947.1 943.7 998.4 4210.8 -1.67772e+007 double(template) 938.8 947.1 20.1 17.4 0 float (template) 945.2 945.5 996.9 4251.5 -1.67772e+007 double(template specialization) 1021.9 1021.7 25.1 23.5 0 float(template specialization) 1078.4 1063.7 1132.9 4979.7 -1.67772e+007 floatの乗算・除算が目も当てられない状態になってしまいました。
多分実際に実行時に計算を行うとその,「目も当てられない状態」は結構真実に近いんじゃないかと.
詳しくはこのあたりを参照のこと.
- /fp (浮動小数点の動作の指定)
- Microsoft Visual C++ Floating-Point Optimization
- Method for getting /Op like consistency in MS C++ 14.0
コンパイル時計算の回避方法については,ちょっと知識に怪しいところがあったのでまた後ほど.
(追記)
fenv_access を ON にして /fp:precise を使用すると、浮動小数点式のコンパイル時間の評価などの一部の最適化が無効になります。
Using /fp:precise with fenv_access ON disables some optimizations such as compile-time evaluations of floating point expressions.
浮動小数点プラグマの使い方が無効です: fenv_access プラグマは precise モードでのみ操作します
とあるので,precise モードで fenv_access ON とすると,コンパイル時評価“も”無効になるようですね.