環境:VC++6.0
浮動小数点算術演算関数の計算速度について


[libc 数学関数の実行速度]

機械学習や確率推論アルゴリズムでは、学習や導出演算の途中に大量の exp, log 演算が
出てくることも珍しくありません。そして、そのようなアルゴリズムは当然ながら重いため、
大規模シミュレーションのためには、いかに軽いアルゴリズムを構築するかに知恵が絞られます。

一般にこのような非線形演算は実行にたくさんのクロック数がかかるわけですが、
そのクロック数を比較しようとしてもデータはなかなか見つかりません。
もっとも処理系依存な面もあるので(たとえば Atom と Core i7 では 実行クロック数が異なるはず)
仕方ない面もあるでしょうが、いずれにせよ、数値計算に不慣れなものにとっては、
非線形演算を比較検討するのはやや面倒なものです。たとえば、log と exp を 10回演算するか、
5次までの多項式近似を採用するか、逆数の3次近似の逆数を取るか... など、
どれが高精度でどれが高速か、すぐにはわかりませんね。

そこでこのページでは、いくつかの非線形関数に関して、実行スピードを実測して表にまとめました。

実行環境は(Windows 7 / Core i5 650 @3.2G / MS VC++6.0)で、
関数は(x*y(積算; ベースライン), log, exp, x^(-1), sqrt, sin, cos, tan, pow, など )です。
標準的な libc での調査なので、SIMD や MMX 命令を使うとどの程度高速化できるかは
また別問題です。(その場合、FPU 命令に log などはないので、精度と相談して
適当に多項式近似することになりますが。)


[結果]

10,000,000 回あたり消費時間

[最適化 -O2; double]
| x*y | log  | exp  | pow  | inv  | sqrt | sin  | cos  | tan  |
| 0.6 | 17.5 | 29.6 | 57.4 | 6.6  |  9.6 | 30.3 | 30.1 | 34.8 |

[最適化 -O2; float]
| x*y | log  | exp  | pow  | inv  | sqrt | sin  | cos  | tan  |
| 0.6 | 18.8 | 55.0 | 60.2 | 6.7  | 10.1 | 30.8 | 30.1 | 34.6 |



[積算何回分に相当するか; double]
| x*y | log  | exp  | pow  | inv  | sqrt | sin  | cos  | tan  |
| 1.0 | 29.2 | 49.3 | 95.7 | 11.0 | 16.0 | 50.5 | 50.2 | 58.0 |


[考察]
(a) どの演算がどれくらい高速か?
積算(fmul)命令と比較すると、他のFP関数演算は 10倍〜100倍 遅いということになります。
それぞれを積算に対する計算時間比で見ると、おおむね
inv(逆数)[x10] < sqrt[x16] < log[x30] < exp、三角関数[x50] < pow[x100]
ということになります。

逆数 inv は 積算より10倍低速ですが、指数関数よりは数倍高速でした。
exp よりも log の方が高速というのは やや意外な結果です。
pow は内部で log や exp を使用していると思われるので、性能は悪いです。
sqrt は指数関数よりも性能が良いというのは、頭の片隅に入れておくべき
かもしれません。

非線形関数 - 逆関数 のペアとしては、演算時間からみれば
(x^2, sqrt(x)) のほうが (exp(x), log(x)) よりも 10倍程度 高速といえます。

三角関数は指数関数よりも遅いです。
cos の方が sin より微妙に高速というのは、なぜでしょうか。

(b) double と float ではどちらが良いか
比較すると、double の方が float より速いです。
これは普通使う FPU が 64bit(倍精度) 向けに作られているためです。
ライブラリも double 型で作られているため、float ではキャストして
使う分だけオーバヘッドが発生してしまいます。
(SIMD命令になると、float 演算もサポートされて、double の 2倍同時に
演算が可能になります。)
特筆すべきは、exp で、double の方が float よりも 4倍近く
明らかに高速になっています。

(c) 非数(NaN, Inf) の発生の影響
一般に、非数が発生すると、明らかにレスポンスが落ちます
たとえば log でみると、引数に非数を入れたり 非数が発生する演算では
通常の演算よりも 4倍ちかく 時間がかかってしまいます。

入力に非数を入れると出力も非数を返すので、繰り返し演算のどこかで
ひとつでも非数が発生してしまうと、それが他の変数へと伝播していき、
最終的にすべての変数が非数に置き換わってしまうことになります。

(d) 最適化の有無での違い


[ソースコード]

(以下参考)

[最適化なし; double]
| x*y | log  | exp  | pow  | inv  | sqrt | sin  | cos  | tan  |
| 2.0 | 31.5 | 53.3 | 68.2 | 6.6  | 17.1 | 39.1 | 37.5 | 47.1 |

| x*y | fabs | (abs) | pow  | sinh | cosh | tanh | sqrt(x2+y2) | _hypot |
| 2.0 | 15.1 |  5.8  | 68.2 | 67.9 | 66.7 | 51.2 | 9.8         | 159.8  |

| log  | log(NaN) | log(0) |
| 32.3 | 122.6    | 128.1  |

[最適化なし; float]
| x*y | log  | exp  | pow  | inv  | sqrt | sin  | cos  | tan  |
| 2.3 | 22.8 | 56.8 | 59.4 | 6.6  |  9.7 | 36.2 | 36.5 | 43.0 |


※ 最適化 なし にした場合 の挙動
 たとえば sqrt(a) より sqrt(a+1.0-1.0) のほうが 速い
 これは なんででだろう

※ 新しいバージョンの MSVCRT は SSE2 対応していて 速いらしい