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;
整数オーバーフローが引き続き発生する可能性があります。
コンパイラはそのような並べ替えを行うことを許可されていますか、それとも結果の不一致に気付き、式の順序をそのままにしておくと信頼できますか?
オプティマイザーがこのような並べ替えを行う場合、それはまだ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_x
およびu32_y
を64ビット符号なしの値に昇格させる必要があります。これは、キャストを追加するのと同じです。 (実際の最適化はCレベルで行われませんが、私は理解している表記法なのでC表記法を使用します。)
コンパイラーは、as if規則の下でのみ並べ替えることができます。つまり、並べ替えが常に指定された並べ替えと同じ結果をもたらす場合、それは許可されます。そうでなければ(あなたの例のように)、そうではありません。
たとえば、次の式が与えられた場合
i32big1 - i32big2 + i32small
これは、大きくても類似していることがわかっている2つの値を減算するように慎重に構成されており、thenが他の小さな値を追加する(したがって、オーバーフローを回避する)ために、コンパイラーは次の順序に並べ替えることができます:
(i32small - i32big2) + i32big1
問題を防ぐために、ターゲットプラットフォームがラップアラウンドで2補数演算を使用しているという事実に依存します。 (このような並べ替えは、コンパイラーがレジスターを要求され、たまたまi32small
は既にレジスタにあります)。
C、C++、およびObjective-Cには「あたかも」ルールがあります。コンパイラは、適合プログラムが違いを認識できない限り、好きなことを実行できます。
これらの言語では、a + b + cは(a + b)+ cと同じであると定義されています。これと例えばa +(b + c)の違いがわかる場合、コンパイラは順序を変更できません。違いがわからない場合、コンパイラーは順序を自由に変更できますが、違いはわからないので問題ありません。
あなたの例では、b = 64ビット、aおよびc 32ビットで、コンパイラーは(b + a)+ cまたはさらに(b + c)+ aを評価することができます。違いはわかりませんが、 (a + c)+ bではありません。違いがわかるからです。
言い換えれば、コンパイラは、コードが本来の動作と異なる動作をすることを許可されていません。生成すると思われるコードや、生成すべきだと思うコードを生成する必要はありませんが、コードwillは、必要な結果を正確に提供します。
標準 からの引用:
[注:演算子は、実際に連想的または可換である場合にのみ、通常の数学規則に従って再グループ化できます。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だった可能性があるためです。ただし、オーバーフローが例外を生成せず、オーバーフローの結果が可逆的であるマシンでは、上記の式ステートメントは同じ結果が発生するため、上記のいずれかの方法で実装によって書き換えられます。 —終了ノート]
コンパイラはそのような並べ替えを行うことを許可されていますか、それとも結果の不一致に気付き、式の順序をそのままにしておくと信頼できますか?
コンパイラは、同じ結果が得られた場合にのみ並べ替えることができます-ここで、あなたが観察したように、そうではありません。
必要に応じて、追加する前にすべての引数をstd::common_type
に昇格する関数テンプレートを作成することができます-これは安全で、引数の順序や手動キャストに依存しませんが、かなり不格好です。
unsigned/int
のビット幅に依存します。
以下の2つは同じではありません(unsigned <= 32
ビットの場合)。 u32_x + u32_y
は0になります。
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
ビットの場合)。整数の昇格により、64ビット演算でu32_x + u32_y
加算が発生しました。順序は関係ありません。
UBです(unsigned == 33
ビットの場合)。整数の昇格により、符号付き33ビット演算で加算が発生し、符号付きオーバーフローはUBです。
コンパイラはそのような並べ替えを許可されていますか?
(32ビットの数学):はいの順序を変更しますが、同じ結果が発生する必要があるため、that 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;