GCCがa * a * a * a * a * aを(a * a * a)*(a * a * a)に最適化しないのはなぜですか?


2120

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

私は好奇心だと、私は交換したときにということであるpow(a,6)a*a*a*a*a*aGCC 4.5.1とオプション「を使用して-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同様の動作になります。

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


13
「pow(a、6)を認識する」とはどういう意味ですか?
Varun Madiath、2011年

659
うーん...あなたは、a a a a a aと(a a a)*(a a * a)が浮動小数点数と同じではないことを知っていますよね?そのためには-funsafe-mathまたは-ffast-mathなどを使用する必要があります。
デイモン

106
:私は、あなたが「浮動小数点演算についてのすべてのコンピュータサイエンティストすることがわかっているが、どのような」デビッド・ゴールドバーグで読むことをお勧めdownload.oracle.com/docs/cd/E19957-01/806-3568/...は、その後、あなたがのより完全な理解を持っていますあなたが入ったばかりのタールピット!
Phil Armstrong

189
完全に合理的な質問です。20年前に私は同じ一般的な質問をし、その単一のボトルネックを解消することで、モンテカルロシミュレーションの実行時間を21時間から7時間に短縮しました。内部ループのコードはプロセスで13兆回実行されましたが、シミュレーションが一夜のウィンドウに表示されました。(以下の回答を参照してください)

23
多分(a*a)*(a*a)*(a*a)ミックスにも投入します。同じ数の乗算ですが、おそらくより正確です。
Rok Kralj、2015

回答:


2738

そのため浮動小数点演算が連想ではありません。浮動小数点乗算でオペランドをグループ化する方法は、回答の数値精度に影響を与えます。

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


10
はい。-ffast-mathでは、このような最適化を行っています。良いアイデア!しかし、私たちのコードは速度よりも正確さを考慮しているので、それを渡さない方が良いでしょう。
xis

19
IIRC C99を使用すると、コンパイラーはこのような「安全でない」FP最適化を実行できますが、GCC(x87以外のもの)は、IEEE 754に従って妥当な試みを行います。これは「エラー範囲」ではありません。正解は1つだけです。
tc。

14
の実装の詳細はpowここにもありません。この答えは参照さえしていませんpow
Stephen Canon

14
@nedR:ICCはデフォルトで再関連付けを許可します。標準準拠の動作を取得する場合は-fp-model precise、ICCで設定する必要があります。 clangそして、gcc厳格な適合WRT再会合にデフォルト設定。
スティーブンキャノン

49
@xis、それは実際に不正確であるというわけで-fassociative-mathはありません。それだけということだa*a*a*a*a*aとは(a*a*a)*(a*a*a)異なっています。正確さについてではありません。それは、標準への準拠と厳密に再現可能な結果、たとえば、どのコンパイラでも同じ結果が得られることです。浮動小数点数はすでに正確ではありません。でコンパイルすることはほとんど不適切-fassociative-mathです。
Paul Draper

652

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)は、a*a*a*a*a*aまたはのどちらよりもはるかに正確です(a*a*a)*(a*a*a)。データを提供するために、Mac Proで小さな実験を行い、[1,2)の間のすべての単精度浮動小数点数についてa ^ 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( )、インライン乗算ツリーを生成することに注意してください。精度とパフォーマンスをトレードオフしたいが、高速演算を有効にしたくない場合に使用します。


29
また、Visual C ++はpow()の「拡張」バージョンを提供していることにも注意してください。呼び出すことによって_set_SSE2_enable(<flag>)flag=1、可能な場合、それはSSE2を使用します。これにより、精度が少し低下しますが、速度が向上します(場合によっては)。MSDN:_set_SSE2_enable()およびpow()
TkTech

18
@TkTech:精度の低下は、使用されているレジスターのサイズではなく、Microsoftの実装によるものです。ライブラリライターのモチベーションが高ければ、32ビットレジスタのみ pow使用して正しく丸められたものを配信することが可能です。SSEベースがありpowます実装より、ほとんどのx87ベースの実装より正確には、その取引の実装はスピードのためのいくつかの精度オフもあります。
スティーブンキャノン

9
@TkTech:もちろん、正確さの低下は、SSEの使用に固有ではなく、ライブラリの作成者が行った選択によるものであることを明確にしたかっただけです。
スティーブンキャノン

7
ここで、相対誤差を計算するための「ゴールドスタンダード」として何を使用したかを知りたいのですが、通常はそうなると思ってa*a*a*a*a*aいましたが、明らかにそうではありません。:)
j_random_hacker 2013

8
@j_random_hacker:私は単精度の結果を比較していたので、ゴールドスタンダードには倍精度で十分です。aで計算し a a a aからdoubleで計算したエラーは、*どの単精度計算のエラーよりもかなり小さいです。
スティーブンキャノン

168

別の同様のケース:ほとんどのコンパイラーは最適化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


10
これはまったく同じではありません。乗算/除算の順序(0による除算を除く)は、合計/減算の順序よりも安全です。私の控えめな意見では、コンパイラーはmults./divsを関連付けようとするべきです。これにより、操作の総数が減り、パフォーマンスが向上するだけでなく、精度も向上します。
CoffeDeveloper 2014

4
@DarioOO:それは安全ではありません。乗算と除算は、指数の加算と減算と同じであり、順序を変更すると、一時変数が指数の可能な範囲を超えやすくなります。(正確には同じではありません。指数が精度の損失を被らないためです...しかし、表現は依然として非常に制限されており、並べ替えは表現できない値につながる可能性があります)
Ben Voigt

8
微積分の背景が不足していると思います。2つの数値を乗算およ​​び除算すると、同じ量のエラーが発生します。2つの数値を減算/加算すると、特に2つの数値の桁数が異なる場合に大きなエラーが発生する可能性があります。したがって、最終的なエラーに小さな変更が生じるため、sub / addよりもmul / divを再配置する方が安全です。
CoffeDeveloper 2015年

8
@DarioOO:mul / divではリスクが異なります:並べ替えによって最終結果にごくわずかな変化が生じるか、指数がオーバーフローして(以前は発生しなかった)、結果が大幅に異なります(潜在的に+ infまたは0)。
Peter Cordes

@GameDeveloper予測できない方法で精度を向上させることは非常に問題があります。
curiousguy

80

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)

これにより、累乗を簡単に入力できるようになり(a括弧で6 秒を入力する必要はありません)、補正された合計(演算の順序が不可欠な例)-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

36
最適なパワーツリーを見つけるのは難しいかもしれませんが、それは小さなパワーに対してのみ興味深いので、明白な答えは、それを1回事前計算し(Knuthが最大100のテーブルを提供)、そのハードコードされたテーブルを使用することです(これは、gccがpowiのために内部で行うことです)。 。
Marc Glisse 2013年

7
最新のプロセッサでは、速度はレイテンシによって制限されます。たとえば、乗算の結果は5サイクル後に利用できる場合があります。そのような状況では、いくつかの力を生み出す最速の方法を見つけることはより難しいかもしれません。
gnasher729 2014年

3
また、相対丸め誤差の下限、または平均相対丸め誤差の平均が最も低いパワーツリーを見つけることもできます。
gnasher729 2014年

1
Boostはこれもサポートしています。例:boost :: math :: pow <6>(n); 共通の要素を抽出することで、乗算の数を減らすことさえ試みていると思います。
gast128 2017

最後のものは(a ** 2)** 3と同等であることに注意してください
minmaxavg

62

GCCは実際にはaが整数のときに最適化a*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フラグはたくさんありますが、特別なものはありません。つまり、stdinから読み取ります。O2最適化レベルを使用します。バイナリの代わりにアセンブリ言語のリストを出力します。リストはIntelアセンブリ言語構文を使用する必要があります。入力はC言語です(通常、言語は入力ファイル拡張子から推測されますが、stdinから読み取るときにファイル拡張子はありません)。標準出力に書き込みます。

出力の重要な部分は次のとおりです。アセンブリ言語で何が行われているのかを示すコメントで注釈を付けました。

; 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

私はLinux Mint 16ペトラ、Ubuntu派生物でシステムGCCを使用しています。これがgccのバージョンです。

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

他の投稿者が指摘したように、浮動小数点演算は結合的でないため、このオプションは浮動小数点では使用できません。


12
2の補数のオーバーフローは未定義の動作であるため、これは整数の乗算に有効です。オーバーフローが発生する場合、並べ替え操作に関係なく、どこかで発生します。したがって、オーバーフローのない式も同じように評価され、オーバーフローする式は未定義の動作なので、コンパイラーがオーバーフローが発生するポイントを変更しても問題ありません。gccもこれをunsigned int行います。
Peter Cordes

51

32ビット浮動小数点数(1.024など)は1.024ではないためです。コンピュータでは、1.024は(1.024-e)から(1.024 + e)までの間隔です。ここで、「e」はエラーを表します。一部の人々はこれに気付かず、* in 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。理想的な数値ではなく間隔で作業していることはすでにわかっているので、これは問題ありません。


52
浮動小数点数は正確です。それらは必ずしもあなたが期待したものと必ずしも同じではありません。さらに、イプシロンを使用した手法自体は、実際の問題に取り組む方法の近似です。なぜなら、実際の予想誤差は仮数のスケールに関連しているためです。つまり、通常、最大で約1 LSBですが、注意していない場合はすべての操作が実行されるため、浮動小数点で重要なことを行う前に、数値解析担当者に相談してください。可能であれば、適切なライブラリを使用してください。
ドナルフェロー2011年

3
@DonalFellows:IEEE規格では、浮動小数点計算が、ソースオペランドが正確な値である場合の結果と最も正確に一致する結果を生成することを要求していますが、実際に正確な値を表すわけではありません。多くの場合、0.1fは(1,677,722 +/- 0.5)/ 16,777,216であると見なす方が、正確な量(1,677,722 +/- 0.5)/ 16,777,216(24桁の10進数で表示されます)。
スーパーキャット

23
@supercat:浮動小数点データ正確な値を表すという点で、IEEE-754はかなり明確です。3.2〜3.4節は関連するセクションです。もちろん、3 +/- 0.5 int x = 3を意味するものとして解釈するように選択できるのと同じように、それらを別の方法で解釈することを選択できますx
スティーブンキャノン

7
@supercat:私は完全に同意しますが、それがDistance数値と正確に等しくないという意味ではありません。これは、数値がモデル化されている物理量の近似にすぎないことを意味します。
Stephen Canon

10
数値解析の場合、浮動小数点数を間隔ではなく正確な値(たまたま求めていた値とは異なる)として解釈すると、脳はあなたに感謝します。たとえば、xが4.5のどこかでエラーが0.1未満の場合、(x + 1)-xを計算すると、「間隔」の解釈では0.8から1.2の間隔が残り、「正確な値」の解釈では結果は1になり、エラーは倍精度で最大2 ^(-50)になります。
gnasher729 2014年

34

フローティング式の縮小について言及しているポスターはまだありません(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


3
長続きする人気のある質問は、時々彼らの年齢を示します。この質問は、GCCが当時の最新のC99標準を厳密に尊重しないことについて免除された可能性がある2011年に尋ねられ、回答されました。もちろん今は2014年なので、GCCは…。
Pascal Cuoq 14年

代わりに、受け入れられた答えがなければ、比較的最近の浮動小数点の質問に答えるべきではありませんか?咳止めstackoverflow.com/questions/23703408
Pascal Cuoq 14年

私はそれを見つけました... gccがC99浮動小数点プラグマを実装していないことは不安です。
David Monniaux、2016年

1
@DavidMonniauxプラグマは、定義により、実装がオプションです。
Tim Seguine

2
@TimSeguineただし、プラグマが実装されていない場合は、そのデフォルト値を実装に最も制限する必要があります。それがダビデが考えていたことだと思います。GCC では、ISO Cモードを使用する場合のFP_CONTRACTについてこれが修正されています。プラグマはまだ実装されていませんが、ISO Cモードではプラグマがオフになっていると見なされます。
vinc17 2018

28

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


3
@greggoいいえ、それでもまだ確定的です。言葉の意味でランダム性は追加されません。
アリス

9
@AliceここでのBjornは、コードの意味で「決定論的」を使用しており、異なるプラットフォームや異なるコンパイラバージョンなどで同じ結果を提供している(外部変数がプログラマの制御の及ばない可能性がある)-不足とは対照的に実行時の実際の数値のランダム性。これがこの言葉の適切な用法ではないと指摘しているのであれば、私はそれについて議論するつもりはありません。
greggo 2014

5
@greggo彼が言ったことのあなたの解釈でさえ、それはまだ間違っています。これがIEEE 754の重要なポイントであり、プラットフォーム全体のほとんど(すべてではない)の操作に同一の特性を提供します。現在、彼はプラットフォームやコンパイラのバージョンについて言及していません。これは、すべてのリモートサーバー/クライアントのすべての操作を同一にしたい場合に有効な懸念事項となりますが、これは彼の説明からは明らかではありません。より良い言葉は「確実に似ている」か何かかもしれません。
アリス

8
@Aliceあなたは、セマンティクスを主張することで、自分の時間も含めて、みんなの時間を無駄にしています。彼の意味は明らかだった。
Lanaru、2014

11
@Lanaru標準の全体のポイントはセマンティクスです。彼の意味は明らかに明確ではなかった。
アリス

28

「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」で置き換えることです。これは、コンパイラベンダーが行うべき唯一の正気なことです。


7
反対投票者は、この回答が完全に適切であることを理解する必要があります。私は自分の答えを裏付けるために数十のソースとドキュメントを引用できます。おそらく、どのダウンボターよりも浮動小数点の精度にもっと関わっています。StackOverflowでは、他の回答ではカバーできない欠落している情報を追加することは完全に合理的です。そのため、丁寧に、理由を説明してください。
CoffeDeveloper 2015年

1
スティーブン・キャノンの答えはあなたが言わなければならないことをカバーしているように私には思えます。あなたはlibmsがスプラインで実装されていると主張しているようです:それらはより一般的には引数の削減(実装されている関数に依存)に加えて、Remezアルゴリズムの多かれ少なかれ洗練されたバリアントによって係数が取得された単一の多項式を使用します。ジャンクションポイントの滑らかさは、libm関数の追求に値する目的とは見なされません(ドメインが十分に正確である場合、ドメインがいくつに分割されているかに関係なく、自動的に非常に滑らかになります)。
Pascal Cuoq、2015年

あなたの答えの後半は、コンパイラがソースコードが言うことを実装するコードを生成することになっているという点を完全に逃しています。また、「正確さ」を意味する場合、「精度」という言葉を使用します。
Pascal Cuoq、2015年

あなたの入力をありがとう、私は答えをわずかに修正しました、何か新しいものが最後の2行にまだ残っています^^
CoffeDeveloper

27

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

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

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

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


17
別のコメンターが指摘したように、これは馬鹿げている点までは真実ではありません。差はコストの半分から10%程度になる可能性があり、タイトなループで実行すると、多くの命令が無駄になり、わずかな精度しか得られない可能性があります。モンテカルロを行っているときに標準のFPを使用すべきではないと言うのは、国中を移動するには常に飛行機を使うべきだと言っているようなものです。多くの外部性を無視します。最後に、これは珍しい最適化ではありません。デッドコード分析とコード削減/リファクタリングは非常に一般的です。
アリス

21

この質問にはすでにいくつかの良い答えがありますが、完全を期すために、C標準の適用可能なセクションは5.1.2.2.3 / 15(これは、 C ++ 11標準)。このセクションでは、演算子が実際に連想的または可換的である場合にのみ、演算子を再グループ化できると述べています。


12

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の累乗のmodが機能するため、オーバーフローが発生した場合でも自由に並べ替えることができるため、さらに簡単です。


1
ゴッドボルトはdouble、int、unsignedとリンクしています。gccとclangはどちらも3つすべてを同じ方法で(を使用して-ffast-math)最適化します
Peter Cordes

@PeterCordesありがとう!
チャールズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.