Clang が x * 1.0 を最適化して x + 0.0 を最適化しないのはなぜですか?



Clang がこのコードのループを最適化する理由


#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

このコードのループではありませんか?


#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(答えがそれぞれ異なるかどうか知りたいので、C と C++ の両方としてタグ付けします。)


答え:


浮動小数点演算の IEEE 754-2008 規格と ISO/IEC 10967 言語非依存演算 (LIA) 規格のパート 1 が、その理由を説明しています。



足し算の場合


デフォルトの丸めモードで (最も近いものに丸め、偶数に同数)x+0.0 であることがわかります x を生成します 、x の場合を除く -0.0 です :その場合、合計がゼロである逆符号の 2 つのオペランドの合計があり、§6.3 パラグラフ 3 の規則により、この追加は +0.0 を生成します。 .


+0.0以降 ビット単位ではありません 元の -0.0 と同じ 、そしてその -0.0 入力として発生する可能性のある正当な値である場合、コンパイラは潜在的な負のゼロを +0.0 に変換するコードを挿入する義務があります .


要約:デフォルトの丸めモードでは、x+0.0 で 、 x の場合



  • 違います -0.0 、次に x それ自体は許容可能な出力値です。

  • -0.0 の場合、出力値はする必要があります +0.0 、これは -0.0 とビットごとに同一ではありません .


かけ算の場合


デフォルトの丸めモードでx*1.0 ではそのような問題は発生しません . x の場合 :



  • は (準) 正規数、x*1.0 == x です。

  • +/- infinityです 、結果は +/- infinity です

  • NaN です 、それから



    つまり、NaN*1.0 の指数と仮数 (符号ではありません) 推奨 入力 NaN から変更されません .記号は上記の §6.3p1 に従って指定されていませんが、実装では、ソース NaN と同一であるように指定できます。 .


  • +/- 0.0です 、結果は 0 です 1.0 の符号ビットと XOR された符号ビット 、§6.3p2に同意。 1.0 の符号ビット以降 0 です 、出力値は入力から変更されません。したがって、x*1.0 == x x でも (負の) ゼロです。


引き算の場合


デフォルトの丸めモードで 、減算 x-0.0 x + (-0.0) と同等であるため、これもノーオペレーションです。 . x の場合 です



  • NaNです の場合、§6.3p1 と §6.2.3 は足し算と掛け算とほぼ同じように適用されます。

  • +/- infinityです 、結果は +/- infinity です

  • は (準) 正規数、x-0.0 == x です。

  • -0.0です 、そして §6.3p2 までに、「[...] 和の符号、または和 x + (−y) とみなされる差 x − y の符号は、せいぜい 1 つの加数と異なる」標識; "。これにより、-0.0 を割り当てる必要があります。 (-0.0) + (-0.0) の結果として 、なぜなら -0.0 none と符号が異なります +0.0 の間、加数の 2 と符号が異なります この条項に違反して、加数の。

  • +0.0です の場合、これは加算ケース (+0.0) + (-0.0) に還元されます 上記の加算の場合で検討 、§6.3p3 により +0.0 を与えると規定されています .


すべての場合において、入力値は出力として正当であるため、x-0.0 を考慮することが許容されます。 ノーオペレーション、および x == x-0.0 トートロジー。


価値を変える最適化


IEEE 754-2008 標準には、次の興味深い引用があります:



すべての NaN とすべての無限大は同じ指数を共有するため、x+0.0 の正しく丸められた結果 および x*1.0 有限 x の場合 x とまったく同じ大きさです 、それらの指数は同じです。


sNaN


シグナリング NaN は浮動小数点のトラップ値です。これらは特殊な NaN 値であり、浮動小数点オペランドとして使用すると無効演算例外 (SIGFPE) が発生します。例外をトリガーするループが最適化されていない場合、ソフトウェアは同じように動作しなくなります。


ただし、user2357112 コメントで指摘しているように 、C11標準は明示的に未定義のままNaNを通知する動作を残します(sNaN )、したがって、コンパイラはそれらが発生しないと想定することが許可されているため、それらが発生させる例外も発生しません。 C++11 標準では、NaN を通知するための動作の記述が省略されているため、未定義のままになっています。


丸めモード


別の丸めモードでは、許容される最適化が変わる場合があります。たとえば、Round-to-Negative-Infinity の下で モード、最適化 x+0.0 -> x 許容されますが、x-0.0 -> x


GCC がデフォルトの丸めモードと動作を想定しないようにするために、実験的フラグ -frounding-math GCC に渡すことができます。


結論


-O3 でも Clang と GCC 、IEEE-754 準拠のままです。これは、IEEE-754 規格の上記の規則に準拠する必要があることを意味します。 x+0.0 ビットが同一ではない x へ すべての x に対して これらのルールの下で、しかし x*1.0 そのように選ばれるかもしれません :つまり、



  1. x のペイロードを変更せずに渡すという推奨事項に従います NaN の場合

  2. NaN の結果の符号ビットを * 1.0 まで変更しない .

  3. x の場合、商/積の間、符号ビットを XOR する順序に従います。 ではない NaN。


IEEE-754-unsafe 最適化 (x+0.0) -> x を有効にするには 、フラグ -ffast-math Clang または GCC に渡す必要があります。


いくつかのコードの回答


#include <time.h>
#include <stdio.h>
static size_t const N = 1 <<
27;
static double arr[N] = { /* initialize to zero */ };
int main() {
clock_t const start = clock();
for (int i = 0;
i <
N;
++i) { arr[i] *= 1.0;
}
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
#include <time.h>
#include <stdio.h>
static size_t const N = 1 <<
27;
static double arr[N] = { /* initialize to zero */ };
int main() {
clock_t const start = clock();
for (int i = 0;
i <
N;
++i) { arr[i] += 0.0;
}
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}