次のプログラムを検討してください。
#include <stdio.h>
int negative(int A) {
return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
if (negative(A)) {
A = ~A + 1;
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
}
if (A < B) return 0;
return 1;
}
int main(){
divide(-2147483648, -1);
}
コンパイラーの最適化なしでコンパイルすると、期待される結果が生成されます。
gcc -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1
コンパイラーの最適化を使用してコンパイルすると、次の誤った出力が生成されます。
gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
私はgcc version 5.4.0
。
ソースコードに変更を加えて、コンパイラが-O3
?
_-2147483648
_は、あなたが思っていることをしません。 Cには負の定数はありません。 _limits.h
_を含め、代わりに_INT_MIN
_を使用します(2の補数マシンのほとんどすべての_INT_MIN
_定義は、正当な理由で_(-INT_MAX - 1)
_として定義します)。
_A = ~A + 1;
_は整数オーバーフローを引き起こすため、_~A + 1
_は未定義の動作を呼び出します。
それはコンパイラではなく、あなたのコードです。
コンパイラは_A = ~A + 1;
_ステートメントを単一のneg
命令、つまり次のコードに置き換えます:
_int just_negate(int A) {
A = ~A + 1;
return A;
}
_
次のようにコンパイルされます。
_just_negate(int):
mov eax, edi
neg eax // just negate the input parameter
ret
_
しかし、コンパイラーは、_A & 0x80000000
_が否定の前にゼロ以外だった場合、否定の後にmustゼロでなければならないことを認識できるほどスマートです未定義の動作に依存していない限り。
これは、2番目のprintf("negative(A) = %d\n", negative(A));
を「安全に」最適化できることを意味します。
_mov edi, OFFSET FLAT:.LC0 // .string "negative(A) = %d\n"
xor eax, eax // just set eax to zero
call printf
_
オンラインの godbolt compiler Explorer を使用して、さまざまなコンパイラの最適化についてアセンブリを確認します。
ここで何が起こっているかを詳しく説明するには:
この回答では、long
が32ビットで、_long long
_が64ビットであると仮定しています。これは最も一般的なケースですが、保証されていません。
Cには符号付き整数定数がありません。 _-2147483648
_は実際には_long long
_型であり、単項マイナス演算子を適用します。
コンパイラは、_2147483648
_が収まるかどうかを確認した後、整数定数の型を選択します。
int
の中に?いいえ、できません。long
の中に?いいえ、できません。long long
_の中に?はい、できます。したがって、整数定数の型は_long long
_になります。次に、その_long long
_に単項マイナスを適用します。long long
_をint
を期待する関数に表示しようとします。優れたコンパイラーがここで警告するかもしれません。暗黙的な変換をより小さな型に強制します(「左辺値変換」)。-2147483648
_はint
の内側に収まるため、変換に実装定義の動作は必要ありません。次に注意が必要なのは、_0x80000000
_を使用する関数negative
です。これはint
でも、_long long
_でもありませんが、_unsigned int
_ではありません(説明については、 こちらを参照 )。
渡されたint
を_unsigned int
_と比較するとき、「通常の算術変換」( これを参照 )はint
を_unsigned int
_。この特定のケースでは結果には影響しませんが、これが_gcc -Wconversion
_ユーザーがここで素晴らしい警告を受け取る理由です。
(ヒント:既に_-Wconversion
_を有効にしてください!微妙なバグをキャッチするのには適していますが、_-Wall
_または_-Wextra
_の一部ではありません。)
次に、値のバイナリ表現のビット単位の逆である_~A
_を実行し、値_0x7FFFFFFF
_で終わります。結局のところ、これは32または64ビットシステムの_INT_MAX
_と同じ値です。したがって、_0x7FFFFFFF + 1
_は、未定義の動作につながる符号付き整数オーバーフローを提供します。これが、プログラムが誤動作している理由です。
意地悪なことに、コードを_A = ~A + 1u;
_に変更すると、整数の昇格が暗黙的に行われるため、突然すべてが期待どおりに動作します。
学んだ教訓:
Cでは、暗黙的な整数の昇格と同様に、整数定数は非常に危険で直感的ではありません。プログラムの意味を微妙に変更し、バグを導入することができます。 Cのすべての操作で、関連するオペランドの実際のタイプを考慮する必要があります。
C11 __Generic
_をいじってみると、実際の型を見るのに良い方法です。例:
_#define TYPE_SAFE(val, type) _Generic((val), type: val)
...
(void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
(void) TYPE_SAFE(0x80000000, int); // won't compile, type is unsigned int
_
このようなバグから身を守るための適切な安全対策は、常にstdint.hを使用し、MISRA-Cを使用することです。
未定義の動作に依存しています。 32ビット符号付き整数の0x7fffffff + 1
は符号付き整数のオーバーフローを引き起こしますが、これは標準に従って未定義の動作であるため、何でも起こります。
Gccでは、-fwrapv
;を渡すことにより、ラップアラウンドの動作を強制できます。それでも、フラグを制御できない場合、より一般的には、より移植性の高いプログラムが必要な場合は、標準でラップアラウンドするために必要なunsigned
整数でこれらのすべてのトリックを行う必要があります(そして符号付き整数とは異なり、ビット演算のセマンティクスが明確に定義されています)。
最初にint
をunsigned
に変換し(標準に従って適切に定義され、期待どおりの結果が得られます)、作業を行い、int
に戻します-実装定義(≠未定義)int
の範囲よりも大きいが、実際には「正しいこと」を行うために2の補数で動作するすべてのコンパイラによって定義されている値の場合。
int divide(int A, int B) {
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
if (negative(A)) {
A = ~((unsigned)A) + 1;
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
}
if (A < B) return 0;
return 1;
}
あなたのバージョン(-O3で):
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
私のバージョン(-O3で):
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1