web-dev-qa-db-ja.com

gccオプティマイザーが誤ったビット操作を生成しないようにするにはどうすればよいですか?

次のプログラムを検討してください。

#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

28
merlin2011
  1. _-2147483648_は、あなたが思っていることをしません。 Cには負の定数はありません。 _limits.h_を含め、代わりに_INT_MIN_を使用します(2の補数マシンのほとんどすべての_INT_MIN_定義は、正当な理由で_(-INT_MAX - 1)_として定義します)。

  2. _A = ~A + 1;_は整数オーバーフローを引き起こすため、_~A + 1_は未定義の動作を呼び出します。

それはコンパイラではなく、あなたのコードです。

84
Art

コンパイラは_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 を使用して、さまざまなコンパイラの最適化についてアセンブリを確認します。

44
Groo

ここで何が起こっているかを詳しく説明するには:

  • この回答では、longが32ビットで、_long long_が64ビットであると仮定しています。これは最も一般的なケースですが、保証されていません。

  • Cには符号付き整数定数がありません。 _-2147483648_は実際には_long long_型であり、単項マイナス演算子を適用します。

    コンパイラは、_2147483648_が収まるかどうかを確認した後、整数定数の型を選択します。

    • intの中に?いいえ、できません。
    • longの中に?いいえ、できません。
    • _long long_の中に?はい、できます。したがって、整数定数の型は_long long_になります。次に、その_long long_に単項マイナスを適用します。
  • 次に、この負の_long long_をintを期待する関数に表示しようとします。優れたコンパイラーがここで警告するかもしれません。暗黙的な変換をより小さな型に強制します(「左辺値変換」)。
    ただし、2の補数を仮定すると、値_-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を使用することです。

17
Lundin

未定義の動作に依存しています。 32ビット符号付き整数の0x7fffffff + 1は符号付き整数のオーバーフローを引き起こしますが、これは標準に従って未定義の動作であるため、何でも起こります。

Gccでは、-fwrapv;を渡すことにより、ラップアラウンドの動作を強制できます。それでも、フラグを制御できない場合、より一般的には、より移植性の高いプログラムが必要な場合は、標準でラップアラウンドするために必要なunsigned整数でこれらのすべてのトリックを行う必要があります(そして符号付き整数とは異なり、ビット演算のセマンティクスが明確に定義されています)。

最初にintunsignedに変換し(標準に従って適切に定義され、期待どおりの結果が得られます)、作業を行い、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
13
Matteo Italia