(A + B + C)≠(A + C + B)とコンパイラの並べ替え


108

2つの32ビット整数を追加すると、整数オーバーフローが発生する可能性があります。

uint64_t u64_z = u32_x + u32_y;

このオーバーフローは、32ビット整数の1つが最初にキャストされるか、64ビット整数に追加される場合に回避できます。

uint64_t u64_z = u32_x + u64_a + u32_y;

ただし、コンパイラが追加の順序を変更することを決定した場合:

uint64_t u64_z = u32_x + u32_y + u64_a;

それでも整数オーバーフローが発生する可能性があります。

コンパイラはそのような並べ替えを行うことができますか、それとも、結果の矛盾に気づき、式の順序をそのまま維持することを信頼できますか?


15
付加uint32_t値のように見えるため、実際には整数オーバーフローは表示されません-オーバーフローしないため、ラップされます。これらは異なる動作ではありません。
Martin Bonnerがモニカをサポートする

5
c ++標準のセクション1.9を参照してください。それはあなたの質問に直接答えます(あなたの質問とほとんど同じ例さえあります)。
Holt

3
@Tal:他の人がすでに述べたように:整数のオーバーフローはありません。Unsignedはラップするように定義されており、Signedの場合は未定義の動作であるため、鼻のデーモンを含むすべての実装で実行されます。
このサイトには正直すぎます。

5
@タル:ナンセンス!私はすでに書いたように:標準は非常に明確であると署名しているが、標準として-のUBであるとして、ラッピング、それが可能である(飽和ではないが必要です。
あまりにも正直、このサイトのために

15
@rustyx:かどうか、あなたが呼び出すラッピングまたはあふれ、それを、ポイントのままであること((uint32_t)-1 + (uint32_t)1) + (uint64_t)0で結果0のに対し、(uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)中の結果0x100000000、およびこれらの2つの値が等しくありません。したがって、コンパイラがその変換を適用できるかどうかは重要です。しかし、そうです、標準では、符号なし整数ではなく、符号付き整数に対してのみ「オーバーフロー」という単語を使用しています。
スティーブジェソップ2016

回答:


84

オプティマイザがそのような並べ替えを行う場合でも、C仕様にバインドされているため、そのような並べ替えは次のようになります。

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

根拠:

で始まる

uint64_t u64_z = u32_x + u64_a + u32_y;

加算は左から右に行われます。

整数の昇格規則は、元の式の最初の加算で、u32_xに昇格することを示していuint64_tます。2番目の追加でu32_yは、にも昇格されuint64_tます。

だから、C仕様に準拠するために、任意のオプティマイザは、促進しなければならないu32_xu32_y64に符号なしの値をビット。これはキャストを追加することと同じです。(実際の最適化はCレベルでは行われませんが、私たちが理解している表記であるため、私はC表記を使用しています。)


左連想ではない(u32_x + u32_t) + u64_aですか?
役に立たない

12
@役に立たない:クラスはすべてを64ビットにキャストしました。現在、注文はまったく違いがありません。コンパイラーは結合性に従う必要はありません。それは、実際に行った場合とまったく同じ結果を生成する必要があるだけです。
gnasher729 2016

2
OPのコードがそのように評価されることを示唆しているようですが、これは真実ではありません。
役に立たない

@Klas- なぜこれが当てはまるのどのようにして正確にコードサンプルに到達するのを説明してください。
rustyx 2016

1
@rustyx説明は必要でした。追加を押してくれてありがとう。
クラス・リンドバック

28

コンパイラは、as ifルールの下でのみ並べ替えが許可されます。つまり、並べ替えによって常に指定した順序と同じ結果が得られる場合は、それが許可されます。それ以外の場合(例のように)、そうではありません。

たとえば、次の式があるとします。

i32big1 - i32big2 + i32small

これは、大きいが類似していることがわかっている2つの値を減算し、次に他の小さい値を追加する(したがって、オーバーフローを回避する)ように注意深く作成されており、コンパイラーは次の順序に並べ替えることができます。

(i32small - i32big2) + i32big1

また、問題を回避するために、ターゲットプラットフォームがラップアラウンド付きの2補数演算を使用しているという事実に依存しています。(コンパイラーがレジスターを要求され、たまたま持っている場合、このような並べ替えは賢明かもしれませんi32smallレジスターにすでにし)。


OPの例では、符号なしの型を使用しています。i32big1 - i32big2 + i32small符号付きの型を意味します。追加の懸念が関係してくる。
chux-モニカを復活させる'25 / 07/25

@chux絶対に。私が書こうとしたポイントは、は書けませんでした(i32small-i32big2) + i32big1が(UBを引き起こす可能性があるため)、動作が正しいことを確信できるため、コンパイラーはそれを効果的に再配置できるということです。
Martin Bonnerがモニカをサポートする

3
@chux:as-ifルールの下でのコンパイラーの再配列について話しているため、UBなどの追加の懸念は関係しません。特定のコンパイラは、独自のオーバーフロー動作を知っていることを利用できます。
MSalters

16

C、C ++、およびObjective-Cには「あたかも」のルールがあります。準拠するプログラムが違いを認識できない限り、コンパイラは好きなように処理できます。

これらの言語では、a + b + cは(a + b)+ cと同じであると定義されています。これとたとえば+(b + c)との違いがわかる場合、コンパイラは順序を変更できません。違いがわからない場合、コンパイラは自由に順序を変更できますが、違いがわからないため、問題ありません。

あなたの例では、b = 64ビット、aおよびc 32ビットの場合、コンパイラは(b + a)+ cまたは(b + c)+ aさえ評価することができます。 (a + c)+ bではありません。違いがわかるからです。

言い換えると、コンパイラーは、コードの動作を本来の動作と異なるものにすることを許可されていません。自分が生成すると思う、または生成する必要があると思うコードを生成する必要はありませんが、コード、本来あるべき結果を正確に提供します。


しかし、大きな注意が必要です。コンパイラは、未定義の動作(この場合はオーバーフロー)がないと見なします。これは、オーバーフローチェックif (a + 1 < a)を最適化する方法と似ています。
csiz

7
@csiz ... 符号付き変数。符号なし変数には、明確に定義されたオーバーフローセマンティクス(ラップアラウンド)があります。
Gavin S. Yancey

7

標準からの引用:

[注:演算子が通常の連想または可換である場合にのみ、通常の数学規則に従って演算子を再グループ化できます。7たとえば、次のフラグメントでは、int a、b;

/∗ ... ∗/
a = a + 32760 + b + 5;

式ステートメントは、次のように動作します

a = (((a + 32760) + b) + 5);

これらの演算子の結合性と優先順位のため。したがって、合計の結果(a + 32760)が次にbに追加され、その結果が5に追加されて、aに割り当てられた値になります。オーバーフローが例外を生成し、intで表現可能な値の範囲が[-32768、+ 32767]であるマシンでは、実装はこの式を次のように書き換えることはできません。

a = ((a + b) + 32765);

aとbの値がそれぞれ-32754と-15の場合、合計a + bは例外を生成しますが、元の式は生成しません。また、式を次のように書き換えることもできません。

a = ((a + 32765) + b);

または

a = (a + (b + 32765));

aとbの値がそれぞれ4と-8または-17と12であった可能性があるためです。ただし、オーバーフローが例外を生成せず、オーバーフローの結果が元に戻せるマシンでは、上記の式ステートメントは同じ結果が発生するため、上記のいずれかの方法で実装によって書き換えられます。—エンドノート]


4

コンパイラはそのような並べ替えを行うことができますか、それとも、結果の矛盾に気づき、式の順序をそのまま維持することを信頼できますか?

コンパイラーは、同じ結果が得られる場合にのみ再順序付けできます-ここで、あなたが観察したように、そうしません。


std::common_type追加する前にすべての引数をプロモートする関数テンプレートを作成することもできます。これは安全で、引数の順序や手動キャストに依存しませんが、かなり扱いにくいです。


明示的なキャストを使用する必要があることは知っていますが、そのようなキャストが誤って省略された場合のコンパイラの動作を知りたいのです。
Tal

1
私が言ったように、明示的なキャストなし:左の加算が最初に実行され、統合された昇格がないため、ラッピングの対象になります。その結果そのほか、おそらく包まれたのは、され、その後に昇格uint64_t一番右の値に加えてのために。
役に立たない2016

as-ifルールについてのあなたの説明は完全に間違っています。たとえばC言語では、抽象マシンで実行する必要がある操作を指定します。"as-if"ルールでは、誰も違いを見分けることができない限り、何でも好きなように実行できます。
gnasher729

つまり、左の連想性と算術変換の規則で示される結果と同じである限り、コンパイラーは何でも実行できます。
役に立たない2016

1

のビット幅に依存しますunsigned/int

下の2つは同じではありません(unsigned <= 32ビットの場合)。 u32_x + u32_y0になります。

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

それらは同じです(unsigned >= 34ビットの場合)。整数プロモーションが発生しました u32_x + u32_y、64ビット演算で加算が発生しました。順序は関係ありません。

UB(unsigned == 33ビット時)です。整数の昇格により、符号付き33ビット演算で加算が発生し、符号付きオーバーフローはUBです。

コンパイラはそのような並べ替えを行うことができますか?

(32ビット数学):再注文はい、同じ結果が発生しなければならない、そうではないことを再注文OPを提案しています。以下は同じです

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

...結果の不一致に気づき、式の順序をそのまま維持すると信頼できますか?

はい、信頼できますが、OPのコーディング目標は明確ではありません。u32_x + u32_yキャリーは寄付する必要がありますか?OPがその貢献を望む場合、コードは

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

だがしかし

uint64_t u64_z = u32_x + u32_y + u64_a;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.