これをコンパイルする:
#include <iostream>
int main()
{
for (int i = 0; i < 4; ++i)
std::cout << i*1000000000 << std::endl;
}
gcc
は次の警告を生成します。
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
符号付き整数オーバーフローがあることを理解しています。
私が得ることができないのは、なぜi
値がそのオーバーフロー操作によって壊れているのですか?
GCCを使用したx86で整数オーバーフローが原因で無限ループが発生するのはなぜですか? の回答を読みましたが、whyこれは起こります-「未定義」は「何でも起こり得る」ことを意味しますが、この特定の動作の根本的な原因は何ですか?
オンライン: http://ideone.com/dMrRKR
コンパイラ:gcc (4.8)
符号付き整数オーバーフロー(厳密に言えば、「符号なし整数オーバーフロー」などはありません)は、undefined behaviourを意味します。そして、これは何でも起こり得ることを意味し、C++のルールの下でそれが起こる理由を議論することは意味がありません。
C++ 11ドラフトN3337:§5.4:1
式の評価中に、結果が数学的に定義されていない場合、またはその型の表現可能な値の範囲内にない場合、動作は未定義です。 [注:C++の既存の実装のほとんどは、整数オーバーフローを無視します。ゼロ除算の処理、ゼロ除数を使用した剰余の形成、およびすべての浮動小数点例外はマシンによって異なり、通常はライブラリ関数によって調整可能です。 —注を終了]
g++ -O3
でコンパイルされたコードは警告を発します(-Wall
がなくても)
a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
a.cpp:9:2: note: containing loop
for (int i = 0; i < 4; ++i)
^
プログラムが何をしているかを分析できる唯一の方法は、生成されたアセンブリコードを読み取ることです。
完全なアセンブリリストは次のとおりです。
.file "a.cpp"
.section .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
.linkonce discard
.align 2
LCOLDB0:
LHOTB0:
.align 2
.p2align 4,,15
.globl __ZNKSt5ctypeIcE8do_widenEc
.def __ZNKSt5ctypeIcE8do_widenEc; .scl 2; .type 32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
.cfi_startproc
movzbl 4(%esp), %eax
ret $4
.cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
.section .text.unlikely,"x"
LCOLDB1:
.text
LHOTB1:
.p2align 4,,15
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
LFB1091:
.cfi_startproc
movl $__ZStL8__ioinit, %ecx
jmp __ZNSt8ios_base4InitD1Ev
.cfi_endproc
LFE1091:
.section .text.unlikely,"x"
LCOLDE1:
.text
LHOTE1:
.def ___main; .scl 2; .type 32; .endef
.section .text.unlikely,"x"
LCOLDB2:
.section .text.startup,"x"
LHOTB2:
.p2align 4,,15
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1084:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %edi
pushl %esi
pushl %ebx
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x70,0x6
.cfi_escape 0x10,0x7,0x2,0x75,0x7c
.cfi_escape 0x10,0x6,0x2,0x75,0x78
.cfi_escape 0x10,0x3,0x2,0x75,0x74
xorl %edi, %edi
subl $24, %esp
call ___main
L4:
movl %edi, (%esp)
movl $__ZSt4cout, %ecx
call __ZNSolsEi
movl %eax, %esi
movl (%eax), %eax
subl $4, %esp
movl -12(%eax), %eax
movl 124(%esi,%eax), %ebx
testl %ebx, %ebx
je L15
cmpb $0, 28(%ebx)
je L5
movsbl 39(%ebx), %eax
L6:
movl %esi, %ecx
movl %eax, (%esp)
addl $1000000000, %edi
call __ZNSo3putEc
subl $4, %esp
movl %eax, %ecx
call __ZNSo5flushEv
jmp L4
.p2align 4,,10
L5:
movl %ebx, %ecx
call __ZNKSt5ctypeIcE13_M_widen_initEv
movl (%ebx), %eax
movl 24(%eax), %edx
movl $10, %eax
cmpl $__ZNKSt5ctypeIcE8do_widenEc, %edx
je L6
movl $10, (%esp)
movl %ebx, %ecx
call *%edx
movsbl %al, %eax
pushl %edx
jmp L6
L15:
call __ZSt16__throw_bad_castv
.cfi_endproc
LFE1084:
.section .text.unlikely,"x"
LCOLDE2:
.section .text.startup,"x"
LHOTE2:
.section .text.unlikely,"x"
LCOLDB3:
.section .text.startup,"x"
LHOTB3:
.p2align 4,,15
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
LFB1092:
.cfi_startproc
subl $28, %esp
.cfi_def_cfa_offset 32
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
addl $28, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
LFE1092:
.section .text.unlikely,"x"
LCOLDE3:
.section .text.startup,"x"
LHOTE3:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
.ident "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSolsEi; .scl 2; .type 32; .endef
.def __ZNSo3putEc; .scl 2; .type 32; .endef
.def __ZNSo5flushEv; .scl 2; .type 32; .endef
.def __ZNKSt5ctypeIcE13_M_widen_initEv; .scl 2; .type 32; .endef
.def __ZSt16__throw_bad_castv; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
Assemblyを読むことさえほとんどできませんが、addl $1000000000, %edi
行を見ることができます。結果のコードは次のようになります
for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
std::cout << i << std::endl;
@ T.C。のこのコメント:
(1)2より大きい値の
i
を持つすべての反復には未定義の動作があるため->(2)最適化のためにi <= 2
と仮定できる->(3)ループ条件は常に真である-> (4)無限ループに最適化されます。
oPのコードのアセンブリコードを、未定義の動作なしで、次のコードのアセンブリコードと比較するというアイデアを私に与えてくれました。
#include <iostream>
int main()
{
// changed the termination condition
for (int i = 0; i < 3; ++i)
std::cout << i*1000000000 << std::endl;
}
そして実際、正しいコードには終了条件があります。
; ...snip...
L6:
mov ecx, edi
mov DWORD PTR [esp], eax
add esi, 1000000000
call __ZNSo3putEc
sub esp, 4
mov ecx, eax
call __ZNSo5flushEv
cmp esi, -1294967296 // here it is
jne L7
lea esp, [ebp-16]
xor eax, eax
pop ecx
; ...snip...
OMG、それは完全に明らかではありません!それは公平ではありません!火事による裁判を要求します!
それに対処し、バグのあるコードを書いたので、気分が悪くなるはずです。結果に耐える。
...または、代わりに、より良い診断とより良いデバッグツールを適切に使用します-それが彼らのためです:
すべての警告を有効にする
-Wall
は、誤検知のない便利な警告をすべて有効にするgccオプションです。これは、最低限使用する必要がある最低限のものです。-Wall
では有効ではありません。誤検知の警告が表示される可能性があるためです。デバッグにデバッグフラグを使用する
-ftrapv
はオーバーフロー時にプログラムをトラップします。-fcatch-undefined-behavior
は、未定義の動作の多くのインスタンスをキャッチします(注:"a lot of" != "all of them"
)明日出荷する必要のある、私が書いたのではないプログラムのスパゲッティ混乱があります!ヘルプ!!!!!! 111oneone
Gccの-fwrapv
を使用
このオプションは、加算、減算、乗算の符号付き算術オーバーフローが2の補数表現を使用して折り返すことを想定するようにコンパイラーに指示します。
1 -§3.9.1.4にあるように、この規則は「符号なし整数オーバーフロー」には適用されません
符号なしと宣言された符号なし整数は、2を法とする算術の法則に従います。n ここで、nは整数の特定のサイズの値表現のビット数です。
そして、例えばUINT_MAX + 1
の結果は数学的に定義されます-2を法とする算術の規則によってn
短い答え、gcc
はこの問題を具体的に文書化していますが、 gcc 4.8リリースノート に(emphasis mine going forward)と書かれていることがわかります:
GCCは、より多くの言語標準によって課せられた制約を使用してループの反復回数の上限を導出するための積極的な分析を使用するようになりました。これにより、SPEC CPU 2006 464.h264refや416.gamessなどの不適合プログラムが期待どおりに動作しなくなる場合があります。この積極的な分析を無効にするために、新しいオプション-fno-aggressive-loop-optimizationsが追加されました。一定の反復回数はわかっているが、最後の反復に到達する前または最後にループ内で未定義の動作が発生することがわかっている一部のループでは、GCCは反復数の上限を導出する代わりにループ内の未定義の動作について警告しますループ用。警告は、-Wno-aggressive-loop-optimizationsで無効にできます。
実際、-fno-aggressive-loop-optimizations
を使用すると、無限ループの動作は停止し、テストしたすべてのケースで停止します。
長い答えは、ドラフトC++標準セクション5
Expressionsを見て、signed integerオーバーフローが未定義の動作であることを知ることから始まります。 /パラグラフ4
式の評価中に結果が数学的に定義されていないか、そのタイプの表現可能な値の範囲にない場合、動作は未定義です。 [注:C++の既存の実装のほとんどは、整数オーバーフローを無視します。ゼロ除算の処理、ゼロ除数を使用した剰余の形成、およびすべての浮動小数点例外はマシンによって異なり、通常はライブラリ関数によって調整できます。 —メモを終了
標準では、未定義の動作は次のように定義されている注記から予測不可能であるとされています。
[注:この国際標準が動作の明示的な定義を省略している場合、またはプログラムが誤った構造または誤ったデータを使用している場合、未定義の動作が予想されることがあります。 許容されない未定義の動作範囲は、予測不可能な結果を伴う状況の完全な無視から、環境に特有の文書化された方法での動作中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行(診断メッセージの発行を伴う)。多くの誤ったプログラム構造は、未定義の動作を引き起こしません。それらは診断される必要があります。 —注を終了]
しかし、これを無限ループに変えるためにgcc
オプティマイザーは何をすることができますか?それは完全に奇抜に聞こえます。しかし、ありがたいことにgcc
は警告でそれを理解する手がかりを与えてくれます。
warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
std::cout << i*1000000000 << std::endl;
^
手がかりはWaggressive-loop-optimizations
ですが、それはどういう意味ですか?幸いなことに、この最適化がこの方法でコードを壊したのはこれが初めてではなく、幸運です。JohnRegehrが記事でケースを文書化したためです GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks 次のコードを示します。
int d[16];
int SATD (void)
{
int satd = 0, dd, k;
for (dd=d[k=0]; k<16; dd=d[++k]) {
satd += (dd < 0 ? -dd : dd);
}
return satd;
}
記事によると:
未定義の動作は、ループを終了する直前にd [16]にアクセスしています。 C99では、配列の末尾から1つ上の位置にある要素へのポインターを作成することはできますが、そのポインターを逆参照してはなりません。
後で言う:
詳細は、次のとおりです。 Cコンパイラは、d [++ k]を見ると、未定義の動作が発生するため、インクリメントされたkの値が配列境界内にあると想定できます。ここのコードでは、GCCはkが0..15の範囲にあると推測できます。少し後で、GCCがk <16を認識すると、「あぁ–その式は常に真であるため、無限ループになります。」ここで、コンパイラーは明確な定義の仮定を使用します有用なデータフローの事実を推測するには、
そのため、コンパイラが実行しなければならないことは、符号付き整数のオーバーフローが未定義の動作であるため、i
が常に4
より小さくなければならず、したがって無限ループがあることを前提としています。
彼はこれが悪名高い LinuxカーネルのNULLポインターチェックの削除 に非常に似ていると説明しています。
struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
gcc
は、s
がs->f;
で参照され、nullポインターの参照解除は未定義の動作であるため、s
はnullであってはならず、したがって、次の行のif (!s)
チェックを最適化することを推測しました。
ここでの教訓は、最新のオプティマイザーは未定義の動作を悪用することに非常に積極的であり、ほとんどの場合より積極的になることです。明らかに、ほんの数例で、オプティマイザーがプログラマーにとって完全に不合理に見えることをしているのを見ることができますが、オプティマイザーの観点から見れば意味があります。
tl; drコードは、integer+正の整数==負の整数。通常、オプティマイザーはこれを最適化しませんが、std::endl
が次に使用される特定のケースでは、コンパイラーはこのテストを最適化します。 endl
の特別な点はまだわかりません。
-O1以上のレベルのアセンブリコードから、gccがループを次のようにリファクタリングすることは明らかです。
i = 0;
do {
cout << i << endl;
i += NUMBER;
}
while (i != NUMBER * 4)
正しく機能する最大の値は715827882
、つまりfloor(INT_MAX/3
)です。 -O1
のアセンブリスニペットは次のとおりです。
L4:
movsbl %al, %eax
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
addl $715827882, %esi
cmpl $-1431655768, %esi
jne L6
// fallthrough to "return" code
注、-1431655768
は2の補数の4 * 715827882
です。
-O2
を押すと、次のように最適化されます。
L4:
movsbl %al, %eax
addl $715827882, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
cmpl $-1431655768, %esi
jne L6
leal -8(%ebp), %esp
jne L6
// fallthrough to "return" code
そのため、行われた最適化は、単にaddl
が上に移動しただけです。
代わりに715827883
を使用して再コンパイルすると、-O1バージョンは変更された数値とテスト値を除いて同一です。ただし、-O2は変更を行います。
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2
cmpl $-1431655764, %esi
に-O1
があった場合、-O2
の行は削除されました。オプティマイザーは、715827883
を%esi
に追加しても-1431655764
に等しくならないことを決定している必要があります。
これはかなり不可解です。それをINT_MIN+1
に追加するとdoes期待される結果が生成されるため、オプティマイザーは%esi
がINT_MIN+1
にならないことを決定している必要があり、なぜ決定するのかわかりませんそれ。
作業例では、数値に715827882
を追加するとINT_MIN + 715827882 - 2
に等しくなり得ないと結論付けることも同様に有効であると思われます。 (これは、実際にラップアラウンドが発生する場合にのみ可能です)、それでもその例ではラインアウトを最適化しません。
私が使用していたコードは次のとおりです。
#include <iostream>
#include <cstdio>
int main()
{
for (int i = 0; i < 4; ++i)
{
//volatile int j = i*715827883;
volatile int j = i*715827882;
printf("%d\n", j);
std::endl(std::cout);
}
}
std::endl(std::cout)
が削除されると、最適化は行われなくなります。実際、std::endl
がインライン化されていても、std::cout.put('\n'); std::flush(std::cout);
で置き換えると最適化は行われません。
std::endl
のインライン化は、ループ構造の初期の部分に影響を与えるようです(これが何をしているのかはよくわかりませんが、誰か他の人が行った場合に備えてここに投稿します)。
元のコードと-O2
を使用:
L2:
movl %esi, 28(%esp)
movl 28(%esp), %eax
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl __ZSt4cout, %eax
movl -12(%eax), %eax
movl __ZSt4cout+124(%eax), %ebx
testl %ebx, %ebx
je L10
cmpb $0, 28(%ebx)
je L3
movzbl 39(%ebx), %eax
L4:
movsbl %al, %eax
addl $715827883, %esi
movl %eax, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl %eax, (%esp)
call __ZNSo5flushEv
jmp L2 // no test
std::endl
、-O2
を手動でインライン化すると:
L3:
movl %ebx, 28(%esp)
movl 28(%esp), %eax
addl $715827883, %ebx
movl $LC0, (%esp)
movl %eax, 4(%esp)
call _printf
movl $10, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZNSo3putEc
movl $__ZSt4cout, (%esp)
call __ZNSo5flushEv
cmpl $-1431655764, %ebx
jne L3
xorl %eax, %eax
これら2つの違いは、オリジナルでは%esi
が使用され、2番目のバージョンでは%ebx
が使用されることです。一般的に%esi
と%ebx
の間に定義されたセマンティクスに違いはありますか? (x86アセンブリについてはあまり知りません)。
Gccで報告されるこのエラーの別の例は、一定の反復回数実行するループがあるが、以下のようなアイテムの数が少ない配列へのインデックスとしてカウンター変数を使用している場合です。
int a[50], x;
for( i=0; i < 1000; i++) x = a[i];
コンパイラは、このループが配列「a」の外部のメモリにアクセスしようとすることを決定できます。コンパイラは、このかなり不可解なメッセージでこれについて不平を言っています。
反復xxuは未定義の動作を呼び出します[-Werror = aggressive-loop-optimizations]
私が得ることができないのは、なぜそのオーバーフロー操作によって値が壊れているのですか?
4回目の反復で整数オーバーフローが発生するようです(i = 3
の場合)。 signed
整数オーバーフローは未定義の動作を呼び出します。この場合、何も予測できません。ループは4
回だけ反復するか、無限または他の何かに進む可能性があります!
結果は、コンパイラごとに異なる場合があり、同じコンパイラの異なるバージョンでも異なる場合があります。
この国際規格が要件を課していない行動
[。 予測できない結果を伴う状況を完全に無視することから、環境に特有の文書化された方法での診断またはプログラム実行中の動作(診断メッセージの発行の有無にかかわらず)、翻訳または実行の終了まで、許容される未定義の動作範囲(診断メッセージの発行)。多くの誤ったプログラム構造は、未定義の動作を引き起こしません。それらは診断される必要があります。 —注を終了]