私は科学的応用に対していくつかの数値最適化をしています。私が気づいたことの1つは、GCCがpow(a,2)
呼び出しをa*a
にコンパイルすることによって最適化することですが、pow(a,6)
呼び出しは最適化されず、実際にライブラリ関数pow
を呼び出すため、パフォーマンスが大幅に低下します。 (対照的に、 インテルC++コンパイラー 、実行可能ファイルicc
は、pow(a,6)
のためのライブラリ呼び出しを排除します。)
私が興味を持っているのは、GCC 4.5.1とオプション "a*a*a*a*a*a
"を使ってpow(a,6)
を-O3 -lm -funroll-loops -msse4
に置き換えたとき、それが5つのmulsd
命令を使うということです。
movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
私が(a*a*a)*(a*a*a)
を書くならば、それは作り出します
movapd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm14, %xmm13
mulsd %xmm13, %xmm13
これは乗算命令の数を3に減らします。icc
も同様の振る舞いをします。
なぜコンパイラはこの最適化のトリックを認識しないのですか?
浮動小数点演算は連想式ではないため、 です。浮動小数点乗算でオペランドをグループ化する方法は、答えの数値精度に影響します。
その結果、ほとんどのコンパイラは、答えが同じであることが確実である場合を除いて、または数値の正確さを気にしないと言わない限り、浮動小数点計算の順序変更について非常に保守的です。例えば、 -fassociative-math
オプション gccの浮動小数点演算への再関連付けを許可するgcc、または-ffast-math
オプションを使用すると、速度に対する精度の妥協がさらに厳しくなります。
Lambdageek は、連想性が浮動小数点数には当てはまらないため、a*a*a*a*a*a
を(a*a*a)*(a*a*a)
に「最適化」すると値が変わる可能性があることを正しく指摘しています。これがC99で許可されていない理由です(ユーザーが特に許可しない限り、コンパイラーフラグまたはプラグマを介して)。一般的に、プログラマーは自分が何をしたのかを理由で書いたと仮定し、それを尊重しなければなりません。 (a*a*a)*(a*a*a)
が欲しい場合は、それを書いてください。
しかし、それは書くのが面倒かもしれません。あなたがpow(a,6)
を使うとき、なぜコンパイラが[あなたが正しいと思うこと]を正しくできないのか?それが間違った事になるからです。優れた数学ライブラリを持つプラットフォームでは、pow(a,6)
はa*a*a*a*a*a
または(a*a*a)*(a*a*a)
よりもはるかに正確です。データをいくつか提供するために、私は自分のMac Proで小さな実験を行い、[1,2]の間のすべての単精度浮動小数点数に対して^ 6を評価する際の最悪の誤差を測定しました。
worst relative error using powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using a*a*a*a*a*a: 2.58e-07
乗算ツリーの代わりにpow
を使用すると、係数4で囲まれるエラーが減少します。ユーザがそうするようにライセンスされていない限り(例えば-ffast-math
を介して)、コンパイラはエラーを増加させる「最適化」をしてはいけません(そして一般的にはしません)。
GCCは、インライン乗算ツリーを生成する必要がある__builtin_powi(x,n)
の代替としてpow( )
を提供しています。正確性とパフォーマンスをトレードオフしたいが高速演算を有効にしたくない場合は、これを使用してください。
別の同様のケース:ほとんどのコンパイラはa + b + c + d
を(a + b) + (c + d)
に最適化せず(2番目の式はより良いパイプライン化が可能なのでこれは最適化です)、与えられたように(すなわち(((a + b) + c) + d)
として)評価します。これもコーナーケースが原因です。
float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));
これは1.000000e-05 0.000000e+00
を出力します
Fortran(科学計算用に設計されたもの)には組み込みの演算子があり、私の知る限りでは、Fortranコンパイラーは通常、あなたが説明したのと同じように整数のべき乗に最適化するでしょう。 C/C++には残念ながらべき乗演算子がなく、ライブラリ関数pow()
しかありません。これはスマートコンパイラがpow
を特別に扱って特別な場合のためのより速い方法でそれを計算することを妨げるものではありませんが、彼らはあまり一般的ではないようです...
数年前、私は整数のべき乗を最適な方法で計算することをより便利にしようとしていました、そして、以下を思いつきました。 CではなくC++ですが、それでもコンパイラが物事を最適化/インライン化する方法についていくらか賢いことにかかっています。とにかく、あなたがそれが実際に役立つと思うかもしれないことを願っています:
template<unsigned N> struct power_impl;
template<unsigned N> struct power_impl {
template<typename T>
static T calc(const T &x) {
if (N%2 == 0)
return power_impl<N/2>::calc(x*x);
else if (N%3 == 0)
return power_impl<N/3>::calc(x*x*x);
return power_impl<N-1>::calc(x)*x;
}
};
template<> struct power_impl<0> {
template<typename T>
static T calc(const T &) { return 1; }
};
template<unsigned N, typename T>
inline T power(const T &x) {
return power_impl<N>::calc(x);
}
奇妙な理由の説明: これはべき乗を計算するための最適な方法を見つけることはできませんが、 最適な解を見つけることはNP完全問題 ですので(pow
を使用するのとは対照的に)、細部にこだわる理由はありません。
それをpower<6>(a)
として使うだけです。
これにより、べき乗を入力するのが簡単になり(6個のa
sを親で綴る必要はありません)、 補正された合計 のように精度に依存する場合は-ffast-math
なしでこの種の最適化を行うことができます。操作の順序は重要です。
おそらく、これがC++であることを忘れて、Cプログラムで使用するだけでよいでしょう(C++コンパイラーでコンパイルする場合)。
これが役に立つことを願っています。
編集:
これは私が私のコンパイラから得たものです:
a*a*a*a*a*a
の場合、
movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
(a*a*a)*(a*a*a)
の場合、
movapd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm1, %xmm0
mulsd %xmm0, %xmm0
power<6>(a)
の場合、
mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm0, %xmm1
Aが整数の場合、GCCは実際には a a a a aを(a a a) (a a a)に最適化します。私はこのコマンドで試してみました:
$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -
たくさんのgccフラグがありますが、空想的なものは何もありません。それらは次のような意味です。 O2最適化レベルを使用してください。バイナリの代わりにアセンブリ言語のリストを出力します。リストにはインテルのアセンブリ言語の構文を使用する必要があります。入力はC言語です(通常、言語は入力ファイル拡張子から推測されますが、標準入力から読み込むとファイル拡張子はありません)。そして標準出力に書き込みます。
これが出力の重要な部分です。私はアセンブリ言語で何が起こっているのかを示すいくつかのコメントでそれに注釈を付けました:
; x is in edi to begin with. eax will be used as a temporary register.
mov eax, edi ; temp = x
imul eax, edi ; temp = x * temp
imul eax, edi ; temp = x * temp
imul eax, eax ; temp = temp * temp
私はUbuntu派生物であるLinux Mint 16 Petra上でシステムGCCを使用しています。これがgccのバージョンです。
$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
他のポスターが指摘したように、浮動小数点演算は実際には結合的ではないので、このオプションは浮動小数点では不可能です。
32ビットの浮動小数点数(1.024など)は1.024ではないためです。コンピュータでは、1.024は(1.024-e)から(1.024 + e)までの間隔です。ここで、 "e"はエラーを表します。これを理解していない人もいれば、* a * a * aに任意の精度の数値を掛け合わせることを意味しますが、それらの数値にエラーはありません。何人かの人々がこれに気付かない理由はおそらく彼らが小学校で行使した数学の計算である:エラーを伴わずに理想的な数でのみ作業し、そして乗算を実行する間単に「e」を無視するのはOKであると信じる。彼らは、 "float a = 1.2"、 "a * a * a"、および同様のCコードに暗黙のうちに "e"があるのを見ません。
C式a * a * a * a * a * aが実際には理想的な数ではうまくいかないという考えを大多数のプログラマーが認識し(そして実行することができれば)、GCCコンパイラーは自由になり、a * aを最適化できます。 * a * a * a * a "は、" t =(a * a); t * t * t "と言い、これはより少ない数の乗算を必要とする。しかし、残念なことに、GCCコンパイラは、コードを書いているプログラマーが "a"がエラーの有無を問わず数字であると考えるかどうかを知りません。そのため、GCCはソースコードと同じようにしか実行できません。GCCがその「裸眼」で見るものだからです。
...どのようなプログラマーを知っていれば you であれば、 " - ffast-math"スイッチを使ってGCCに "やあ、GCC、私は何をしているのかわかっている!"これはGCCがa * a * a * a * a * aを別のテキストに変換することを可能にします - それはa * a * a * a * a * aとは異なるように見えます - しかしまだエラー間隔内の数を計算しますa * a * a * a * a * aこれは問題ありません。あなたはすでに自分が区間を使って作業していることを知っているからです。理想的な数値ではありません。
浮動表現の縮小については、まだどのポスターでも言及されていません(ISO C標準、6.5p8および7.12.2)。 FP_CONTRACT
プラグマがON
に設定されている場合、コンパイラーはa*a*a*a*a*a
のような式を単一の丸めで正確に評価されるかのように単一の操作として見なすことができます。例えば、コンパイラはそれをより速くより正確な内部のべき乗関数で置き換えるかもしれません。エンドユーザによって提供されるコンパイラオプションが時々誤って使用されるかもしれない間、振る舞いがソースコードで直接プログラマによって部分的に制御されるので、これは特に興味深いです。
FP_CONTRACT
プラグマのデフォルトの状態は実装定義であるため、コンパイラはデフォルトでそのような最適化を実行できます。したがって、IEEE 754の規則に従う必要がある移植性のあるコードでは、明示的にOFF
に設定する必要があります。
コンパイラがこのプラグマをサポートしていない場合、開発者がそれをOFF
に設定することを選択した場合、そのような最適化を避けることによって保守的でなければなりません。
GCCはこのプラグマをサポートしていませんが、デフォルトのオプションではON
と見なします。したがって、ハードウェアFMAを使用するターゲットでは、a*b+c
からfma(a、b、c)への変換を防ぎたい場合は、-ffp-contract=off
(プラグマを明示的にOFF
に設定する)または-std=c99
(GCCに通知) C標準版、ここではC99に準拠するには、上記の段落に従ってください)。これまで、後者のオプションは変換を妨げていませんでした。つまり、GCCはこの点に準拠していませんでした。 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845
私はこのケースがまったく最適化されるとは思わなかったでしょう。式全体に、操作全体を削除するために再グループ化できる部分式が含まれていることはあまりありません。まれに遭遇するEdgeのケースをカバーするのではなく、著しい改善をもたらす可能性がより高い分野に彼らの時間を投資することを私はコンパイラ作家が期待するでしょう。
私は他の答えからこの表現が確かに適切なコンパイラスイッチで最適化されることができるということを知って驚いた。最適化は些細なことか、もっと一般的な最適化のEdgeの場合か、コンパイラの作成者が非常に徹底的だったかのどちらかです。
ここで行ったように、コンパイラにヒントを提供しても問題はありません。それがどのような違いをもたらすかを見るためにステートメントと式を並べ替えることは、マイクロ最適化プロセスの正常で期待される部分です。
(適切なスイッチなしで)矛盾した結果を出すためにコンパイラが2つの式を考慮することは正当化されるかもしれませんが、その制限に束縛される必要はありません。違いは信じられないほど小さいでしょう - 違いがあなたにとって重要であるならば、そもそも標準的な浮動小数点演算を使うべきではないので。
Lambdageekが指摘したように、フロート乗算は連想的ではなく、精度は落ちますが、精度が上がると、決定論的なアプリケーションが必要になるため、最適化に反対することもできます。たとえば、すべてのクライアントが同じ世界をシミュレートしなければならないゲームシミュレーションクライアント/サーバーでは、浮動小数点計算を決定論的にする必要があります。
通常、「pow」などのライブラリ関数は、可能な限り最小限のエラーが発生するように慎重に作成されます(一般的な場合)。これは通常、関数をスプラインで近似することで達成されます(Pascalのコメントによれば、最も一般的な実装は Remezアルゴリズム を使用しているようです)
基本的に次の操作:
pow(x,y);
任意の単一の乗算または除算のエラーとほぼ同じの固有エラーがあります。
次の操作中:
float a=someValue;
float b=a*a*a*a*a*a;
5倍の単一の乗算または除算(5つの乗算を組み合わせているため)のエラーよりも大きい固有のエラーがあります。
コンパイラーは、実行している最適化の種類に本当に注意する必要があります。
pow(a,6)
をa*a*a*a*a*a
に最適化すると、mayパフォーマンスが向上しますが、浮動小数点数の精度が大幅に低下します。a*a*a*a*a*a
をpow(a,6)
に最適化すると、 "a"はエラーなしで乗算できる特別な値(2の累乗または小さな整数)であるため、実際に精度が低下する可能性がありますpow(a,6)
を(a*a*a)*(a*a*a)
または(a*a)*(a*a)*(a*a)
に最適化する場合、pow
関数と比較して精度が低下する可能性があります。一般に、任意の浮動小数点値の場合、「pow」は最終的に記述できる関数よりも精度が高いことを知っていますが、特別な場合には複数の乗算で精度とパフォーマンスが向上する場合があり、開発者がより適切なものを選択するか、最終的にコードをコメント化し、他の誰もそのコードを「最適化」しないようにします。
意味をなす唯一のこと(個人的な意見、および明らかに特定の最適化またはコンパイラフラグなしのGCCの選択)を最適化するには、「pow(a、2)」を「a * a」に置き換える必要があります。これは、コンパイラベンダーがすべき唯一の正気なことです。
この質問にはすでにいくつかの良い答えがありますが、完全を期すために、C標準の該当するセクションは5.1.2.2.3/15(これは、このセクションの1.9/9と同じです)と指摘したいと思います。 C++ 11標準)この節では、演算子は本当に連想型または可換型である場合にのみ再グループ化できると述べています。
gccは実際には浮動小数点数に対してもこの最適化を行うことができます。例えば、
double foo(double a) {
return a*a*a*a*a*a;
}
になる
foo(double):
mulsd %xmm0, %xmm0
movapd %xmm0, %xmm1
mulsd %xmm0, %xmm1
mulsd %xmm1, %xmm0
ret
-O -funsafe-math-optimizations
で。ただし、この並べ替えはIEEE-754に違反しているため、フラグが必要です。
Peter Cordesがコメントで指摘したように、符号付き整数は-funsafe-math-optimizations
なしでこの最適化を行うことができます。オーバーフローがない場合は正確に成り立ち、オーバーフローがある場合は未定義の動作になるからです。だからあなたは得る
foo(long):
movq %rdi, %rax
imulq %rdi, %rax
imulq %rdi, %rax
imulq %rax, %rax
ret
-O
だけで。符号なし整数の場合、2のべき乗のべき乗で動作し、オーバーフローに直面しても自由に並べ替えることができるため、さらに簡単です。