GCCを使用するx86で整数オーバーフローが無限ループを引き起こすのはなぜですか?


129

次のコードは、GCCの無限ループに入ります。

#include <iostream>
using namespace std;

int main(){
    int i = 0x10000000;

    int c = 0;
    do{
        c++;
        i += i;
        cout << i << endl;
    }while (i > 0);

    cout << c << endl;
    return 0;
}

だからここでの取り決めです:符号付き整数オーバーフローは技術的に未定義の動作です。しかし、x86上のGCCは、オーバーフローでラップするx86整数命令を使用して整数演算を実装します。

したがって、未定義の動作であるにもかかわらず、オーバーフローでラップすることを期待していました。しかし、明らかにそうではありません。それで私は何を逃したのですか?

私はこれを使ってこれをコンパイルしました:

~/Desktop$ g++ main.cpp -O2

GCC出力:

~/Desktop$ ./a.out
536870912
1073741824
-2147483648
0
0
0

... (infinite loop)

最適化を無効にすると、無限ループはなくなり、出力は正しいものになります。Visual Studioもこれを正しくコンパイルし、次の結果を提供します。

正しい出力:

~/Desktop$ g++ main.cpp
~/Desktop$ ./a.out
536870912
1073741824
-2147483648
3

その他のバリエーションは次のとおりです。

i *= 2;   //  Also fails and goes into infinite loop.
i <<= 1;  //  This seems okay. It does not enter infinite loop.

関連するすべてのバージョン情報は次のとおりです。

~/Desktop$ g++ -v
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ..

...

Thread model: posix
gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4) 
~/Desktop$ 

だから問題は:これはGCCのバグですか?または、GCCが整数演算を処理する方法について何か誤解しましたか?

*このバグはCで再現されると想定しているため、このCにもタグを付けています(まだ確認していません)。

編集:

これがループのアセンブリです:(私が適切に認識した場合)

.L5:
addl    %ebp, %ebp
movl    $_ZSt4cout, %edi
movl    %ebp, %esi
.cfi_offset 3, -40
call    _ZNSolsEi
movq    %rax, %rbx
movq    (%rax), %rax
movq    -24(%rax), %rax
movq    240(%rbx,%rax), %r13
testq   %r13, %r13
je  .L10
cmpb    $0, 56(%r13)
je  .L3
movzbl  67(%r13), %eax
.L4:
movsbl  %al, %esi
movq    %rbx, %rdi
addl    $1, %r12d
call    _ZNSo3putEc
movq    %rax, %rdi
call    _ZNSo5flushEv
cmpl    $3, %r12d
jne .L5

10
これは、から生成されたアセンブリコードを含めると、はるかにわかりやすくなりますgcc -S
Greg Hewgill、2011年

組み立ては驚くほど長いです。それでも編集する必要がありますか?
Mysticial、2011年

ループに関連する部分だけお願いします。
Greg Hewgill、2011年

12
-1。これは厳密に言えば未定義の動作であると言い、これが未定義の動作であるかどうかを尋ねます。これは私にとって本当の質問ではありません。
ヨハネスシャウブ-litb 2013年

8
@ JohannesSchaub-litbコメントありがとうございます。おそらく私の側の言葉遣いが悪い。解約投票を獲得する方法を明確にするために最善を尽くします(それに応じて質問を編集します)。基本的に、私はそれがUBであることを知っています。しかし、私はx86上のGCCがx86整数命令を使用することも知っています-オーバーフローでラップします。したがって、UBであるにもかかわらず、ラップすることを期待していました。しかし、そうではなかったので混乱しました。したがって問題。
Mysticial 2013年

回答:


178

規格で未定義の動作と記載されている場合それはそれを意味しますます。何でも起れる。「すべて」には「通常は整数が循環しますが、時々奇妙なことが起こる」ことが含まれます。

はい、x86 CPUでは、整数は通常、期待どおりにラップされます。これはそれらの例外の1つです。コンパイラは、ユーザーが未定義の動作を引き起こさないと想定し、ループテストを最適化します。あなたが本当にラップアラウンドをしたい場合は、合格-fwrapvg++またはgccコンパイルするとき。これにより、明確に定義された(2の補数)オーバーフローセマンティクスが得られますが、パフォーマンスが低下する可能性があります。


24
ああすごい。知らなかった-fwrapv。これを指摘してくれてありがとう。
Mysticial 2011年

1
偶発的な無限ループに気づこうとする警告オプションはありますか?
Jeff Burdges、2011年

5
:私は-Wunsafe-ループの最適化は、ここで述べたstackoverflow.com/questions/2982507/...
ジェフBurdges

1
-1「はい、x86 CPUでは通常、整数は期待どおりにラップされます。」それは間違っている。しかしそれは微妙です。オーバーフローでトラップを発生させることは可能であることを思い出しますが、それはここ話していることではありません。それ以外では、x86 bcd演算(C ++では許可されていない表現)を無視して、x86整数演算は常に2の補数であるため、ラップします。x86整数演算のプロパティについて、g ++の誤った(または非常に非現実的で意味のない)最適化を誤解しています。
乾杯とhth。-Alf

5
@ Cheersandhth.-Alf、「x86 CPUで」つまり、「Cコンパイラを使用してx86 CPU向けに開発しているとき」を意味します。私は本当にそれを綴る必要がありますか?明らかに、コンパイラーとGCCに関する私のすべての話は、アセンブラーで開発している場合は無関係です。その場合、整数オーバーフローのセマンティクスは、非常に明確に定義されています。
bdonlan 2012年

18

それは簡単です:未定義の動作-特に最適化(-O2)がオンになっている場合-はが起こってもよいことを意味ます。

あなたのコードは-O2スイッチなしで(あなたの)期待通りに動作します。

ちなみにiclとtccでも問題なく動作しますが、そのようなものに依存することはできません...

よれば、この、GCCの最適化は、実際には符号付き整数オーバーフローを利用します。これは、「バグ」が仕様によるものであることを意味します。


それは、コンパイラが未定義の動作のためにすべてのものの無限ループを選ぶことをちょっと吸います。
2010年

27
@逆:同意しません。未定義の動作で何かをコーディングしている場合は、無限ループになるように祈ってください。検出が容易になります...
Dennis

コンパイラが積極的にUBを探しているなら、壊れたコードを超最適化する代わりに例外を挿入してみませんか?
2010年

15
@Inverse:コンパイラーは未定義の動作を積極的に探していません。発生しないことを前提としています。これにより、コンパイラはコードを最適化できます。たとえば、を計算する代わりにfor (j = i; j < i + 10; ++j) ++k;、が設定されます。k = 10これは、符号付きオーバーフローが発生しない場合は常に trueになるためです。
Dennis

@Inverseコンパイラは何も「選択」しませんでした。ループはコードで記述しました。コンパイラはそれを発明しませんでした。
2013

13

ここで注意すべき重要な点は、C ++プログラムはC ++抽象マシン(通常、ハードウェア命令によってエミュレートされる)用に作成されていることです。x86用にコンパイルしているという事実は、これが未定義の動作をしているという事実とはまったく関係ありません。

コンパイラーは、未定義の動作の存在を自由に使用して、最適化を改善します(この例のように、ループから条件を削除します)。実行時にマシンコードがC ++抽象マシンが要求する結果を生成するという要件は別として、C ++レベルの構造とx86レベルのマシンコードの構造の間のマッピングは保証されていません。


5
i += i;

//オーバーフローは定義されていません。

-fwrapvを使用すると、それは正しいです。-fwrapv


3

人をお願いします、未定義の動作はまさにそれ、未定義ですです。それは何が起こる可能性があることを意味します。実際には(この場合のように)、コンパイラーはそれを自由に想定できます。ができないをます。呼び出され、それがコードをより速く/より小さくすることができるならそれが好きなことをしてください。実行してはならないコードで何が起こるかは、誰もが推測しています。それは周囲のコードに依存します(それに応じて、コンパイラーは異なるコードを生成する可能性があります)、使用される変数/定数、コンパイラーのフラグなど...コンパイラーは更新されて同じコードを異なる方法で書き込むか、またはコード生成について別の見方をした別のコンパイラーを入手してください。または、別のマシンを入手するだけで、同じアーキテクチャラインの別のモデルでも、独自の未定義の動作が発生する可能性があります(未定義のオペコードを検索します。一部の進取的なプログラマーは、これらの初期のマシンの一部で有用な処理が行われることを発見しました...) 。有るません「コンパイラーは未定義の動作に対して明確な動作を提供します」。実装によって定義される領域があり、コンパイラが一貫して動作することを期待できるはずです。


1
はい、未定義の動作とは何かをよく知っています。ただし、特定の環境で言語の特定の側面がどのように実装されているかを理解していると、特定の種類のUBだけが表示され、他の種類のUBは表示されないことが期待できます。GCCが整数演算をx86整数演算として実装していることを知っています-オーバーフローでラップします。だから私はそのような行動を想定しました。私が予期していなかったことは、bdonlanが答えたように、GCCが他のことをすることでした。
Mysticial 2013年

7
違う。何が起こるかというと、GCCは未定義の振る舞いを呼び出さないものと想定することが許可されているので、GCCはそれが起こらなかったかのようにコードを出力するだけです。それ発生した場合未定義の動作なしで要求された動作を実行するための命令が実行され、結果はCPUが行うことです。つまり、x86はx86に関するものです。別のプロセッサの場合は、まったく異なる処理を実行する可能性があります。または、コンパイラが十分に賢いので、未定義の動作を呼び出していることを理解し、nethackを開始することができます(そうです、gccのいくつかの古いバージョンはまさにそれを行いました)。
フォンブランド2013年

4
私のコメントを間違って読んだと思います。私は言った:「私が予想しなかったこと」-それが私がそもそも質問をした理由です。私はしませんでした GCCは、任意のトリックを引くことを期待しています。
Mysticial 2013年

1

コンパイラが整数のオーバーフローを「非クリティカル」形式の未定義動作(付録Lで定義)と見なす必要があると指定したとしても、整数のオーバーフローの結果は、より具体的な動作の特定のプラットフォームの約束がない場合、最低でも「部分的に不確定な値」と見なされます。このような規則の下では、1073741824 + 1073741824を追加すると、2147483648、-2147483648、または2147483648 mod 4294967296に一致したその他の値が任意に生成されたと見なされ、追加によって得られた値は、0 mod 4294967296に一致した任意の値と見なされます。

オーバーフローが「部分的に不確定な値」を生成することを許可するルールは、Annex Lの文字と精神に従うために十分に明確に定義されますが、オーバーフローが制約されていない場合に正当化されるのと同じように、コンパイラーが一般的に役立つ推論を行うことを妨げません。未定義の動作。コンパイラーが偽の「最適化」を行うのを防ぎます。多くの場合、その主な効果は、プログラマーがそのような「最適化」を防ぐことを唯一の目的とするコードに余分な混乱を追加することを要求することです。それがいいことかどうかは、自分の視点による。

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