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 なぜそうなのかを答えてください。
IEEE 754§6.3符号ビット
入力または結果のいずれかがNaNの場合、この規格はNaNの符号を解釈しません。ただし、ビット文字列(copy、negate、abs、copySign)の操作では、NaNオペランドの符号ビットに基づいてNaN結果の符号ビットを指定することに注意してください。論理述語totalOrderも、NaNオペランドの符号ビットの影響を受けます。他のすべての操作について、この標準では、入力NaNが1つしかない場合、またはNaNが無効な操作から生成される場合でも、NaN結果の符号ビットを指定しません。
入力も結果もNaNでない場合、積または商の符号は、オペランドの符号の排他的OR;和または差x-yの符号は合計x +(−y)は、最大で1つの加数の符号とは異なり、変換の結果の符号、量子化操作、roundTo-Integral操作、およびroundToIntegralExact(5.3.1を参照)はこれらのルールは、オペランドまたは結果がゼロまたは無限の場合でも適用されます。
反対符号の2つのオペランドの合計(または同様の符号の2つのオペランドの差)が正確にゼロの場合、その合計(または差)の符号は、roundTowardNegativeを除くすべての丸め方向属性で+0になります。その属性の下では、正確なゼロサム(または差)の符号は-0でなければなりません。ただし、x + x = x −(−x)は、xがゼロの場合でもxと同じ符号を保持します。
デフォルトの丸めモードの下で(Round-to-Nearest、Ties-to-Even)、we x+0.0
がx
を生成することを参照してください。ただし、x
が-0.0
の場合を除きます。この追加により+0.0
が生成される3つのルール。
+0.0
は元の-0.0
とbitwise同一ではなく、-0.0
は入力として発生する正当な値であるため、コンパイラーは潜在的な負のゼロを+0.0
に変換するコードを挿入する必要があります。
要約:x
の場合、デフォルトの丸めモードでは、x+0.0
で
-0.0
の場合、x
自体が許容可能な出力値です。-0.0
の場合、出力値は+0.0
である必要があります。 -0.0
とビット単位で同一ではありません。デフォルトの丸めモードでは、x*1.0
ではこのような問題は発生しません。 x
の場合:
x*1.0 == x
常に(非)通常の数値です。+/- infinity
の場合、結果は同じ符号の+/- infinity
になります。NaN
であり、
IEEE 754§6.2.3 NaNの伝播
結果にNaNオペランドを伝搬し、入力として単一のNaNを持つ操作は、宛先形式で表現可能な場合、入力NaNのペイロードでNaNを生成する必要があります。
これは、NaN*1.0
の指数と仮数(符号ではないが)が、入力NaN
から変更されない推奨であることを意味します。上記の6.3p1に従って符号は指定されていませんが、実装はソースNaN
と同一であると指定する場合があります。
+/- 0.0
の場合、結果は6.3 [p2と一致して、その符号ビットが0
の符号ビットとXORされた1.0
です。 1.0
の符号ビットは0
であるため、出力値は入力から変更されません。したがって、x
が(負の)ゼロの場合でも、x*1.0 == 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
は、この節に違反して、加数のtwoと符号が異なります。+0.0
の場合、これは上記ので考慮される加算ケース(+0.0) + (-0.0)
に還元されます。加算のケース、§6.3p3では+0.0
。すべての場合において、入力値は出力として正当であるため、x-0.0
をノーオペレーション、x == x-0.0
をトートロジーと見なすことができます。
IEEE 754-2008規格には、次の興味深い引用があります。
IEEE 754§10.4リテラルの意味と値を変更する最適化
[...]
次の値を変更する変換は、とりわけ、ソースコードの文字通りの意味を保持します。
- Xがゼロではなく、シグナルNaNではなく、結果の指数がxと同じ場合、アイデンティティプロパティ0 + xを適用します。
- XがシグナリングNaNではなく、結果の指数がxと同じである場合、アイデンティティプロパティ1×xを適用します。
- クワイエットNaNのペイロードまたは符号ビットを変更します。
- [...]
すべてのNaNおよびすべての無限大は同じ指数を共有し、有限のx
に対するx+0.0
およびx*1.0
の正しく丸められた結果はx
とまったく同じ大きさであるため、それらの指数は同じ。
シグナリングNaNは浮動小数点トラップ値です。これらは、浮動小数点オペランドとして使用すると無効な演算例外(SIGFPE)が発生する特別なNaN値です。例外をトリガーするループが最適化された場合、ソフトウェアは同じ動作をしなくなります。
ただし、user2357112コメントで指摘のように、C11標準ではシグナルNaN(sNaN
)の動作が未定義のままであるため、コンパイラは、それらが発生しないと仮定することが許可されているため、発生する例外も発生しません。 C++ 11標準では、NaNのシグナリングの動作の記述が省略されているため、定義されていません。
代替丸めモードでは、許容される最適化が変更される場合があります。たとえば、Round-to-Negative-Infinityモードでは、最適化x+0.0 -> x
は許可されますが、x-0.0 -> x
は禁止されます。
GCCがデフォルトの丸めモードと動作を想定しないようにするために、実験フラグ-frounding-math
をGCCに渡すことができます。
Clangおよび [〜#〜] gcc [〜#〜] は、-O3
であっても、IEEE-754に準拠したままです。これは、IEEE-754標準の上記の規則を守らなければならないことを意味します。 x+0.0
は、これらのルールの下のすべてのx
に対してx
とビットが同一ではないが、x*1.0
はそうなるように選択されるかもしれません:つまり、
x
のペイロードを変更せずに渡すことをお勧めします。* 1.0
だけ変更されないままにします。x
がnotNaNの場合、商/製品中のXOR符号ビット)の順序に従います。IEEE-754-unsafe最適化(x+0.0) -> x
を有効にするには、フラグ-ffast-math
をClangまたはGCCに渡す必要があります。
x
が x += 0.0
の場合、-0.0
はNOOPではありません。ただし、結果は使用されないため、オプティマイザーはループ全体を削除できます。一般的に、オプティマイザーが決定を行う理由を判断するのは困難です。