x87 FPU で厳密な単精度演算を行うコスト

senna_hppの日記より.

DirectXから入ったので、浮動小数点演算にはfloat型演算を使うようになったのですが、実際にはdouble型の方が速いそうなのでその確認のための検証実験。

(中略)

typeadd(ms)sub(ms)multi(ms)div(ms)
double203.6201.95.65.4
flaot913.4912.9674.0675.1
double(template)201.1201.35.15.4
float (template)913.8915.0680.2675.5
double(template specialization)201.7201.56.54.7
float(template specialization)913.6912.2674.0674.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の結果

typeadd(ms)sub(ms)multi(ms)div(ms)計算結果
double962.3962.821.720.40
flaot947.1943.7998.44210.8-1.67772e+007
double(template)938.8947.120.117.40
float (template)945.2945.5996.94251.5-1.67772e+007
double(template specialization)1021.91021.725.123.50
float(template specialization)1078.41063.71132.94979.7-1.67772e+007

floatの乗算・除算が目も当てられない状態になってしまいました。

多分実際に実行時に計算を行うとその,「目も当てられない状態」は結構真実に近いんじゃないかと.
詳しくはこのあたりを参照のこと.

コンパイル時計算の回避方法については,ちょっと知識に怪しいところがあったのでまた後ほど.



(追記)

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 とすると,コンパイル時評価“も”無効になるようですね.