1. ホーム
  2. c++

[解決済み] なぜClangはx * 1.0を最適化し、x + 0.0を最適化しないのですか?

2022-07-11 20:05:55

質問

なぜ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 Language Independent Arithmetic (LIA) 規格、第 1 部 がその理由を回答しています。

IEEE 754 §6.3 符号ビット

入力または結果のいずれかがNaNのとき、本標準はNaNの符号を解釈しない。しかし、ビット文字列の操作(copy、negate、abs、copySign)は、NaNオペランドの符号ビットに基づいて、NaN結果の符号ビットを指定することがあることに注意してください。論理述語totalOrderも、NaNオペランドの符号ビットに影響される。他のすべての操作について、この規格は、1つの入力NaNがある場合、またはNaNが無効な操作から生成される場合であっても、NaN結果の符号ビットを指定しない。

入力も結果もNaNでないとき、積または商の符号はオペランドの符号の排他的論理和であり、和、または和x+(-y)とみなされる差x-yの符号は、最大でも加算器の符号の1つと異なり、和の符号は加算器の符号の1つと異なる。 和の符号又は和x+(-y)とみなされる差x-yの符号は,加算符号のうちの最大1つと異なる;及び 変換,量子化演算,roundToIntegral演算及びroundToIntegralExact (5.3.1 参照 ) の結果の符号は,最初の又は唯一のオペランドの符号とする。これらの規則は,オペランド又は結果がゼロ又は無限である場合にも適用されるものとする。

反対の符号をもつ二つのオペランドの和(又は同符号をもつ二つのオペランドの差)が正確にゼロであるとき,その和(又は差)の符号は,roundTowardNegative以外のすべての丸め方向属性において+0でなければならず,その属性では,正確にゼロの和(又は差)の符号は-0でなければならない。 しかし,x+x=x-(-x)はxがゼロであってもxと同じ符号を保持する。

足し算の場合

デフォルトの丸めモードでは (ニアラウンド、タイズトゥイーブン) で、以下のようになります。 x+0.0x ただし 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 であれば、以下に従って

    IEEE 754 §6.2.3 NaN伝搬

    NaN オペランドをその結果に伝搬し、入力として単一の NaN を持つ演算は、宛先形式で表現可能であれば、入力 NaN のペイロードを持つ NaN を生成する必要があります。

    の指数と仮数(符号は違うが)は、NaNになることを意味します。 NaN*1.0 推奨 は、入力から変更されないように NaN . 符号は上記§6.3p1に従って不特定ですが、実装では、ソース NaN .

  • +/- 0.0 であれば、結果は 0 の符号ビットと XOR し、その符号ビットが 1.0 の符号ビットとXORされています。の符号ビットは 1.00 であれば、出力値は入力から変化しない。このように 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 とは符号が異なるからです。 なし とは符号が異なりますが +0.0 とは符号が異なる。 と異なり、この条項に違反します。
  • +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+0.0x*1.0 に対して、有限の x と全く同じ大きさです。 x と同じ大きさであり、その指数も同じです。

sNaNs

Signaling NaNs は浮動小数点トラップ値で、浮動小数点オペランドとして使用すると無効な演算例外 (SIGFPE) が発生する特別な NaN 値です。例外をトリガーするループが最適化された場合、ソフトウェアの動作はもはや同じではありません。

しかし、user2357112 のように がコメントで指摘しているように のように、C11 標準は NaN のシグナリングの挙動を明示的に未定義のままにしています ( sNaN ) の動作は未定義であるため、コンパイラーは NaN が発生しないと仮定してよく、したがって NaN が発生する例外も発生しないと仮定してよいことになっています。C++11 標準では、NaN をシグナリングする動作の記述が省略されているため、これも未定義のままになっています。

丸めモード

代替丸めモードでは、許容される最適化が変更される場合があります。例えば 負の無限大に丸める モードでは、最適化 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 が選ばれるかもしれません。 : すなわち、私たちが

  1. のペイロードを変更せずに渡すという勧告に従いましょう。 x がNaNである場合、ペイロードを変更せずに渡すという勧告に従うこと。
  2. NaN の結果の符号ビットを変更しないようにするには * 1.0 .
  3. 以下の場合、商/積の間に符号ビットを XOR するという命令に従います。 x ではない はNaNです。

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