2236

科学アプリケーションで数値最適化を行っています。私が気づいたことの 1 つは、GCC が呼び出しpow(a,2)を にコンパイルして最適化することですa*aが、呼び出しpow(a,6)は最適化されておらず、実際にはライブラリ関数powを呼び出すため、パフォーマンスが大幅に低下します。(対照的に、インテル C++ コンパイラー、実行可能ファイルiccは、のライブラリー呼び出しを排除しますpow(a,6)。)

私が興味を持っているのは、 GCC 4.5.1 とオプション " "pow(a,6)を使用して置き換えた場合、5 つの命令が使用されることです。a*a*a*a*a*a-O3 -lm -funroll-loops -msse4mulsd

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ます。

コンパイラがこの最適化トリックを認識しないのはなぜですか?

4

12 に答える 12

2848

Floating Point Math は Associative ではないためです。浮動小数点乗算でオペランドをグループ化する方法は、答えの数値精度に影響します。

その結果、ほとんどのコンパイラは、答えが同じままであると確信できない場合、または数値の精度を気にしないと伝えない限り、浮動小数点計算の並べ替えについて非常に保守的です。たとえば、gcc が浮動小数点演算を再関連付けできるようにする gcc のオプションや、精度と速度のさらに積極的なトレードオフを可能にするオプション-fassociative-mathです-ffast-math

于 2011-06-21T18:56:51.430 に答える
691

Lambdageekは、結合性が浮動小数点数には当てはまらないため、a*a*a*a*a*ato(a*a*a)*(a*a*a)値が変わる可能性があることを正しく指摘しています。これが、C99 で許可されていない理由です (ユーザーがコンパイラ フラグまたはプラグマを介して特に許可しない限り)。一般に、プログラマーは何らかの理由で自分が行ったことを書いたと想定されており、コンパイラーはそれを尊重する必要があります。あなたが望むなら(a*a*a)*(a*a*a)、それを書いてください。

ただし、それを書くのは面倒です。を使用すると、なぜコンパイラは[あなたが考える]正しいことをすることができないのpow(a,6)ですか? それは間違ったことをするからです。優れた数学ライブラリを備えたプラットフォームでは、はまたはpow(a,6)よりもはるかに正確です。いくつかのデータを提供するために、Mac Pro で小さな実験を行い、[1,2) 間のすべての単精度浮動小数点数に対して a^6 を評価する際の最悪の誤差を測定しました。a*a*a*a*a*a(a*a*a)*(a*a*a)

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 分の1 に減少します。コンパイラは、エラーを増加させる「最適化」を行うべきではありません (通常は行いません-ffast-math)。

GCC は、インライン乗算ツリーを生成する__builtin_powi(x,n)の代替として提供することに注意してください。pow( )精度とパフォーマンスをトレードオフしたいが、高速計算を有効にしたくない場合に使用します。

于 2011-06-22T15:32:18.863 に答える
185

別の同様のケース: ほとんどのコンパイラは最適化せずa + b + c + d( (a + b) + (c + d)2 番目の式をより適切にパイプライン処理できるため、これは最適化です)、与えられたものとして (つまり as として(((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

于 2011-06-22T22:39:13.297 に答える
84

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て、この種の最適化を行うことができます(演算の順序が重要な例)。 .-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
于 2011-06-23T11:44:53.050 に答える
52

1.024 などの 32 ビット浮動小数点数は 1.024 ではないためです。コンピューターでは、1.024 は (1.024-e) から (1.024+e) までの間隔で、「e」はエラーを表します。一部の人々はこれを認識できず、また、a*a の * は任意精度の数値の乗算を意味し、それらの数値にエラーが付随していないと信じています。これに気が付かない人がいるのは、小学校時代の算数の計算で、誤差をつけずに理想数だけを扱い、「e」を無視してかけ算をしてもいいと思っていたからかもしれません。彼らは、"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 が「肉眼」で見ているものだからです。

... 自分がどのようなプログラマーかがわかったら「-ffast-math」スイッチを使用して、GCC に「やあ、GCC、自分が何をしているかわかっている!」と伝えることができます。これにより、GCC は a*a*a*a*a*a を別のテキストに変換できるようになります - a*a*a*a*a*a とは異なって見えますが、それでも以下のエラー間隔内で数値を計算しますa*a*a*a*a*a. 理想的な数値ではなく、間隔で作業していることは既にわかっているので、これで問題ありません。

于 2011-06-23T10:07:41.793 に答える
39

浮動式の縮約について言及しているポスターはまだありません (ISO C 標準、6.5p8 および 7.12.2)。FP_CONTRACTプラグマが に設定されている場合、コンパイラは、1 回の丸めで正確に評価されたかのONように、式を 1 回の演算とみなすことができます。a*a*a*a*a*aたとえば、コンパイラはそれを、より高速でより正確な内部べき乗関数に置き換えることができます。エンド ユーザーが提供するコンパイラ オプションが誤って使用される場合がある一方で、プログラマが直接ソース コードで動作を部分的に制御できるため、これは特に興味深いものです。

FP_CONTRACTプラグマのデフォルト状態は実装定義であるため、コンパイラはデフォルトでそのような最適化を行うことができます。したがって、IEEE 754 規則に厳密に従う必要がある移植可能なコードは、明示的に に設定する必要がありOFFます。

コンパイラがこのプラグマをサポートしていない場合、開発者がOFF.

GCC はこのプラグマをサポートしていませんが、デフォルトのオプションでは、次のように想定されますON。したがって、ハードウェア FMA を持つターゲットの場合、a*b+cfma(a,b,c) への変換を防止したい場合は、 -ffp-contract=off(プラグマを明示的にC標準バージョン、ここではC99、したがって上記の段落に従います)。以前は、後者のオプションは変換を妨げていませんでした。つまり、GCC はこの点に準拠していませんでした: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845OFF-std=c99

于 2014-06-27T21:03:11.707 に答える
30

"pow" のようなライブラリ関数は通常、可能な限り最小限のエラーが発生するように慎重に作成されます (一般的な場合)。これは通常、関数をスプラインで近似することで実現されます (Pascal のコメントによると、最も一般的な実装はRemez アルゴリズムを使用しているようです) 。

基本的に次の操作:

pow(x,y);

には、単一の乗算または除算の誤差とほぼ同じ大きさの固有の誤差があります。

次の操作中:

float a=someValue;
float b=a*a*a*a*a*a;

単一の乗算または除算の誤差の 5 倍を超える固有の誤差があります(5 つの乗算を組み合わせているため)。

コンパイラは、実行している最適化の種類に非常に注意する必要があります。

  1. 最適化pow(a,6)するa*a*a*a*a*aとパフォーマンス向上する可能性がありますが、浮動小数点数の精度が大幅に低下します。
  2. 「a」はエラーなしで乗算できる特別な値(2の累乗または小さな整数)であるため、それに最適化a*a*a*a*a*a すると実際に精度が低下する場合がありますpow(a,6)
  3. に最適化pow(a,6)する場合、(a*a*a)*(a*a*a)または関数(a*a)*(a*a)*(a*a)と比較して精度が低下する可能性がありpowます。

一般に、任意の浮動小数点値の場合、「pow」は最終的に記述できるどの関数よりも精度が高いことを知っていますが、いくつかの特別なケースでは、複数の乗算の方が精度とパフォーマンスが向上する可能性があり、より適切なものを選択するのは開発者次第です。最終的にコードにコメントを付けて、他の誰もそのコードを「最適化」しないようにします。

最適化するのが理にかなっている唯一のこと (個人的な意見、および特定の最適化またはコンパイラ フラグを除いた GCC での選択) は、「pow(a,2)」を「a*a」に置き換えることです。それは、コンパイラ ベンダーがすべき唯一の正しいことです。

于 2015-01-03T16:40:39.073 に答える
30

Lambdageek が指摘したように、浮動小数点乗算は連想的ではなく、精度が低下する可能性がありますが、精度が向上すると、決定論的なアプリケーションが必要になるため、最適化に反対することもできます。たとえば、ゲーム シミュレーション クライアント/サーバーでは、すべてのクライアントが同じ世界をシミュレートする必要があり、浮動小数点計算を決定論的にする必要があります。

于 2011-06-23T12:44:13.233 に答える
27

このケースが最適化されるとはまったく予想していませんでした。操作全体を削除するために再グループ化できる部分式が式に含まれていることはあまりありません。コンパイラの作成者は、めったに遭遇しないエッジ ケースをカバーするのではなく、顕著な改善をもたらす可能性が高い領域に時間を投資することを期待しています。

他の回答から、この式は適切なコンパイラ スイッチを使用して実際に最適化できることを知って驚きました。最適化が些細なものであるか、より一般的な最適化のエッジ ケースであるか、またはコンパイラの作成者が非常に徹底していたかのいずれかです。

ここで行ったように、コンパイラにヒントを提供しても問題はありません。ステートメントと式を並べ替えて、それらがもたらす違いを確認することは、マイクロ最適化プロセスの通常の予期される部分です。

コンパイラは、(適切なスイッチなしで) 一貫性のない結果を提供する 2 つの式を考慮することを正当化するかもしれませんが、その制限に縛られる必要はありません。違いは信じられないほど小さいので、違いが問題になる場合は、そもそも標準の浮動小数点演算を使用しないでください。

于 2011-06-21T18:52:49.963 に答える