C ++での符号付きオーバーフローと未定義の動作(UB)


56

次のようなコードの使用について疑問に思っています

int result = 0;
int factor = 1;
for (...) {
    result = ...
    factor *= 10;
}
return result;

ループが何n度も繰り返される場合はfactor10正確にn回数が掛けられます。ただし、合計回数をfactor掛けて使用します。ループの最後の反復以外はオーバーフローしないが、ループの最後の反復ではオーバーフローする可能性があると想定する場合、そのようなコードは受け入れ可能ですか?この場合、オーバーフローが発生した後、の値が使用されることはありません。10n-1factorfactor

このようなコードが受け入れられるべきかどうかについて私は議論しています。ループがオーバーフローする可能性がある場合、乗算をifステートメント内に配置し、ループの最後の反復で乗算を実行しないことが可能です。欠点は、コードが煩雑になり、以前のすべてのループ反復をチェックする必要がある不要なブランチが追加されることです。また、ループの反復回数を1回減らし、ループの後にループ本体を1回複製することもできます。これもコードを複雑にします。

問題の実際のコードは、リアルタイムグラフィックスアプリケーションの総CPU時間の大部分を消費するタイトな内部ループで使用されます。


5
この質問はここではなくcodereview.stackexchange.comにあるはずなので、この質問をトピック外として閉じることに投票します。
Kevin Anderson、

31
@KevinAnderson、それはここでは有効ではありません。サンプルコードは修正されるだけでなく、単に改善されるためです。
バトシェバ

1
@harold彼らは近くにぶら下がっています。
にオービットでライトネスレース

1
@LightnessRaceswithMonica:規格の作成者は、さまざまなプラットフォームと目的を意図した実装が、規格が要求するかどうかに関係なく、それらのプラットフォームと目的に役立つ方法でさまざまなアクションを有意義に処理することにより、プログラマーが利用できるセマンティクスを拡張することを意図し、期待していました。また、移植性のないコードを軽視したくないと述べました。したがって、質問間の類似性は、サポートする必要のある実装によって異なります。
スーパーキャット

2
@supercat実装で定義された動作を確認し、ツールチェーンに拡張機能があることがわかっている場合は、使用できます(移植性は気にしません)。UBは?疑わしい。
、オービットでのライトネスレース

回答:


51

コンパイラは、有効なC ++プログラムにUBが含まれていないことを前提としています。例を考えてみましょう:

if (x == nullptr) {
    *x = 3;
} else {
    *x = 5;
}

その場合x == nullptr、それを逆参照して値を割り当てるのはUBです。したがって、これが有効なプログラムで終了する唯一の方法は、x == nullptrがtrueを生成せず、コンパイラーがas ifルールの下で仮定できる場合のみであり、上記は次と同等です。

*x = 5;

今あなたのコードで

int result = 0;
int factor = 1;
for (...) {      // Loop until factor overflows but not more
   result = ...
   factor *= 10;
}
return result;

の最後の乗算はfactor、有効なプログラムでは発生しません(符号付きオーバーフローは定義されていません)。したがって、割り当てもresult発生しません。最後の反復の前に分岐する方法がないため、前の反復も発生しません。最終的に、コードの正しい部分(つまり、未定義の動作が発生しないこと)は次のとおりです。

// nothing :(

6
「未定義の動作」は、プログラム全体にどのように影響するかを明確に説明せずに、SOの回答でよく耳にする表現です。この答えは物事をより明確にします。
Gilles-PhilippePaillé19年

1
そして、がターゲットでのみ呼び出されINT_MAX >= 10000000000INT_MAXが小さい場合に別の関数が呼び出される場合、これは「便利な最適化」になる可能性もあります。
R .. GitHub ICE HELPING ICEを停止

2
@Gilles-PhilippePaillé投稿に固執したい場合があります。 良性データレースは、どれほど厄介なことができるかを記録するための私のお気に入りの1つです。また、MySQLには素晴らしいバグレポートがあり、私が再び見つけることができないようです-誤ってUBを呼び出したバッファオーバーフローチェックです。特定のコンパイラの特定のバージョンは、単にUBが発生しないと想定し、オーバーフローチェック全体を最適化しました。
Cort Ammon、

1
@SolomonSlow:UBに問題がある主な状況は、標準と実装のドキュメントの一部が特定のアクションの動作を説明しているが、標準の他の一部がそれをUBとして特徴付けている状況です。標準が作成される前の一般的な慣習は、コンパイラー作成者がそのようなアクションを有意義に処理することでした。ただし、顧客が何か他のことをすることで利益を得られる場合を除き、標準の作成者がコンパイラー作成者が意図的に他のことを行うとは思っていませんでした。 。
スーパーキャット

2
@Gilles-PhilippePaillé:LLVMブログの未定義の動作についてすべてのCプログラマが知っておくべきことも良いことです。これは、たとえば、符号付き整数オーバーフローUBが、i <= nループがループのように常に無限でないことをコンパイラーに証明させる方法を説明していi<nます。またint i、最初の4G配列要素への可能性のあるラップ配列インデックス付けに署名をやり直す必要がなく、ループ内のポインター幅に昇格します。
Peter Cordes

34

intオーバーフローの動作は未定義です。

factorループ本体の外側を読んでもかまいません。それまでにオーバーフローした場合は、オーバーフローが定義されていない、その前後、および逆説的少し前に、コードの動作が未定義になります。

このコードを維持する際に発生する可能性がある1つの問題は、最適化に関してコンパイラーがますます攻撃的になっていることです。特に、未定義の動作は発生しないと想定する習慣を身につけています。これが事実であるために、彼らはforループを完全に取り除くかもしれません。

unsignedタイプを使用できませんが、両方を含む式factorでのinttoの不要な変換について心配する必要がありunsignedますか?


12
@nicomp; 何故なの?
バトシェバ

12
@Gilles-PhilippePaillé:私の答えは、それが問題があることを教えてくれませんか?私の最初の文は必ずしもOPにあるわけではなく、より広いコミュニティfactorであり、それ自体への割り当てで「使用」されています。
バトシェバ

8
@Gilles-PhilippePailléとこの回答は、なぜ問題があるのか​​を説明しています
idclev 463035818

1
@Bathshebaそうです、私はあなたの答えを誤解しました。
Gilles-PhilippePaillé19年

4
未定義の動作の例として、そのコードがランタイムチェックを有効にしてコンパイルされると、結果を返す代わりに終了します。機能するために診断機能をオフにする必要があるコードが壊れています。
Simon Richter

23

実際のオプティマイザを検討することは洞察に満ちているかもしれません。ループ展開は既知の手法です。opループの展開の基本的な考え方は、

for (int i = 0; i != 3; ++i)
    foo()

舞台裏で実装する方が良いかもしれません

 foo()
 foo()
 foo()

これは固定された境界を持つ簡単なケースです。しかし、最近のコンパイラーは、変数の境界に対してもこれを行うことができます。

for (int i = 0; i != N; ++i)
   foo();

なる

__RELATIVE_JUMP(3-N)
foo();
foo();
foo();

明らかにこれは、コンパイラがN <= 3であることがわかっている場合にのみ機能します。そして、それが元の質問に戻るところです。コンパイラは、符号付きオーバーフローが発生しないことを認識しているため、ループが32ビットアーキテクチャで最大9回実行できることを認識しています。10^10 > 2^32。したがって、9反復ループのアンロールを実行できます。しかし、意図した最大値は10回の反復でした。

発生する可能性があるのは、N = 10のアセンブリ命令(9-N)への相対ジャンプが発生するため、オフセット-1、つまりジャンプ命令自体です。おっとっと。これは明確に定義されたC ++に対して完全に有効なループの最適化ですが、与えられた例はタイトな無限ループに変わります。


9

オーバーフローした値が読み取られるかどうかに関係なく、符号付き整数のオーバーフローは、未定義の動作になります。

多分あなたのユースケースでは、ループから最初の反復を持ち上げ、これを回すことができます

int result = 0;
int factor = 1;
for (int n = 0; n < 10; ++n) {
    result += n + factor;
    factor *= 10;
}
// factor "is" 10^10 > INT_MAX, UB

これに

int factor = 1;
int result = 0 + factor; // first iteration
for (int n = 1; n < 10; ++n) {
    factor *= 10;
    result += n + factor;
}
// factor is 10^9 < INT_MAX

最適化を有効にすると、コンパイラーは上記の2番目のループを1つの条件付きジャンプに展開する可能性があります。


6
これは少し技術的すぎるかもしれませんが、「符号付きオーバーフローは未定義の動作です」は単純化されすぎています。正式には、符号付きオーバーフローがあるプログラムの動作は定義されていません。つまり、標準はそのプログラムが何をするかを教えてくれません。オーバーフローした結果に問題があるというだけではありません。プログラム全体に問題があります。
ピートベッカー

公正な観察、私は私の答えを修正しました。
elbrunovsky

もっと簡単に言えば、最後のイテレーションをはがして死者を取り除くfactor *= 10;
Peter Cordes

9

これはUBです。ISO C ++用語では、プログラム全体の動作全体は、最終的に UB ヒットする実行に対して完全に指定されていません。古典的な例は、C ++標準が気にするところまであり、悪魔をあなたの鼻から飛ばすことができます。(私は鼻の悪魔が現実的な可能性がある実装を使用しないことをお勧めします)。詳細については、他の回答を参照してください。

コンパイラーは、コンパイル時に表示される実行パスについてコンパイル時に「問題を引き起こす」可能性があります。たとえば、これらの基本ブロックに到達しないと想定します。

未定義の動作についてすべてのCプログラマが知っておくべきこと(LLVMブログ)も参照してください。そこで説明したように、signed-overflow UBを使用すると、コンパイラfor(... i <= n ...)は、unknownであっても、ループが無限ループではないことを証明できますn。また、符号拡張をやり直す代わりに、intループカウンターをポインターの幅に「昇格」させることもできます。(そのため、その場合のUBの結果は、配列の低64kまたは4G要素の外側にアクセスする可能性があります(iその値の範囲への符号付きラッピングが予想される場合)。

場合によっては、コンパイラーはx86のような不正な命令をud2ブロックに対して発行するため、実行されるとUBが発生する可能性があります。(関数は決して呼び出されない可能性があることに注意してください。そのため、コンパイラーは通常、他の関数を暴走して破壊することはできません。 UBにつながらないすべての入力)


おそらく最も効率的な解決策は、最後のイテレーションを手動で剥離して、不要な部分factor*=10を回避することです。

int result = 0;
int factor = 1;
for (... i < n-1) {   // stop 1 iteration early
    result = ...
    factor *= 10;
}
 result = ...      // another copy of the loop body, using the last factor
 //   factor *= 10;    // and optimize away this dead operation.
return result;

または、ループ本体が大きい場合は、に符号なしの型を使用することを検討してくださいfactor 次に、符号なし乗算をオーバーフローさせることができ、2の累乗(符号なし型の値ビットの数)まで明確にラップされます。

これは、あなたがそれを使用しても結構です、あなたのunsigned->署名変換がオーバーフローしたことがない場合は特に、署名タイプ。

符号なしと符号付き2の補数の間の変換は無料です(すべての値で同じビットパターン)。C ++標準で指定されているint-> unsignedのモジュロラッピングは、補数や符号/大きさの場合とは異なり、同じビットパターンを使用するだけで簡単になります。

unsigned-> signedも同様に取るに足らないものですが、より大きい値に対しては実装で定義されますINT_MAX。最後のイテレーションからの巨大な符号なしの結果を使用していない場合、心配する必要はありません。しかし、そうであれば、符号なしから符号なしへの変換は未定義ですか?を参照してください。値-doesn'tフィットケースがある実装定義実装が選択しなければならないことを意味し、いくつかの行動を。正気なものは、(必要に応じて)符号なしビットパターンを切り捨て、符号付きとして使用します。これは、範囲内の値に対しても同じように機能するため、追加の作業は必要ないためです。そして、それは間違いなくUBではありません。したがって、大きな符号なし値は負の符号付き整数になる可能性があります。たとえば、int x = u; gccとclangが最適化されない場合x>=0-fwrapv彼らが行動を定義したので、なしでも常に真実であるように。


2
私はここで反対票を理解していません。私は主に最後のイテレーションの剥離について投稿したかった。しかし、質問に答えるために、私はUBを理解する方法についていくつかのポイントをまとめました。詳細については、他の回答を参照してください。
Peter Cordes

5

代わりに、ループ内のいくつかの追加のアセンブリ命令を許容できる場合

int factor = 1;
for (int j = 0; j < n; ++j) {
    ...
    factor *= 10;
}

あなたは書ける:

int factor = 0;
for (...) {
    factor = 10 * factor + !factor;
    ...
}

最後の乗算を避けるため。!factorブランチを導入しません:

    xor     ebx, ebx
L1:                       
    xor     eax, eax              
    test    ebx, ebx              
    lea     edx, [rbx+rbx*4]      
    sete    al    
    add     ebp, 1                
    lea     ebx, [rax+rdx*2]      
    mov     edi, ebx              
    call    consume(int)          
    cmp     r12d, ebp             
    jne     .L1                   

このコード

int factor = 0;
for (...) {
    factor = factor ? 10 * factor : 1;
    ...
}

また、最適化後にブランチレスアセンブリが生成されます。

    mov     ebx, 1
    jmp     .L1                   
.L2:                               
    lea     ebx, [rbx+rbx*4]       
    add     ebx, ebx
.L1:
    mov     edi, ebx
    add     ebp, 1
    call    consume(int)
    cmp     r12d, ebp
    jne     .L2

(GCC 8.3.0でコンパイル-O3


1
ループボディが大きくない限り、最後の反復を単に剥離する方が簡単です。これは巧妙なハックですが、ループで運ばれる依存関係チェーンの待ち時間をfactorわずかに増やします。かではない:それは2倍LEAにコンパイル時にそれだけで行うことがLEA + ADDとして効率的などについてですf *= 10ようf*5*2で、test最初で隠さレイテンシーLEA。ただし、ループ内で追加のuopsが必要になるため、スループットの低下の可能性があります(または、少なくともハイパースレッディング対応の問題)
Peter Cordes

4

forステートメントの括弧内は何であるかを示していませんが、次のようなものであると想定します。

for (int n = 0; n < 10; ++n) {
    result = ...
    factor *= 10;
}

カウンターの増分とループ終了チェックを本体に移動するだけです。

for (int n = 0; ; ) {
    result = ...
    if (++n >= 10) break;
    factor *= 10;
}

ループ内のアセンブリ命令の数は変わりません。

Andrei Alexandrescuのプレゼンテーション「Speed Is Found in the Minds of People」に触発されました。


2

関数を考えてみましょう:

unsigned mul_mod_65536(unsigned short a, unsigned short b)
{
  return (a*b) & 0xFFFFu;
}

公開された理論的根拠によれば、規格の作成者は、この関数が(たとえば)0xC000および0xC000の引数を持つ一般的な32ビットコンピューターで呼び出された場合、オペランドを昇格*signed intせると、計算で-0x10000000が生成されると予想していました。に変換するunsignedと、0x90000000u次の結果が得unsigned shortられunsignedます。昇格した場合と同じ答えになります。それにもかかわらず、gccは、オーバーフローが発生した場合に無意味な動作をする方法でその機能を最適化することがあります。 入力のいくつかの組み合わせがオーバーフローを引き起こす可能性のある-fwrapvコードは、故意に不正な入力の作成者が選択した任意のコードを実行できるようにすることが許容されない限り、オプションで処理する必要があります。


1

これはなぜですか:

int result = 0;
int factor = 10;
for (...) {
    factor *= 10;
    result = ...
}
return result;

or の...ループ本体は実行されません。100以上のみです。これを機能させたい場合は、最初のイテレーションをはがしてまだ開始する必要があります。factor = 1factor = 10factor = 1
Peter Cordes

1

未定義の動作にはさまざまな面があり、許容できるものは使用法によって異なります。

リアルタイムグラフィックアプリケーションでの合計CPU時間の大きなチャンクを消費するタイトな内部ループ

それ自体は少し珍しいことですが、そうかもしれません...これが実際に当てはまる場合、UBはおそらく「許可され、受け入れられる」レルム内にあります。グラフィックプログラミングは、ハッキングや醜いもので悪名高いです。それが「機能する」限り、フレームを生成するのに16.6msより長くかからない限り、通常、誰も気にしません。しかし、それでも、UBを呼び出すことの意味に注意してください。

まず、基準があります。その観点からは、議論する必要はなく、正当化する方法もありません。コードは単に無効です。ifsやwhensはありません。有効なコードではありません。それはあなたの視点から見れば中指であり、とにかく行くのが良い時間の95-99%であると言うこともできます。

次に、ハードウェアの側面があります。これが問題になるいくつかの珍しい奇妙なアーキテクチャがあります。すべてのコンピューターの80%を構成する1つのアーキテクチャー(または一緒にすべてのコンピューターの95%を構成する2つのアーキテクチャー)のオーバーフローは「ええ、何でも、気にしないため、「珍しい、奇妙な」と言っていますハードウェアレベルのもの。あなたは確かにゴミ(まだ予測可能ですが)の結果を得ますが、悪事は起こりません。 それは違います
すべてのアーキテクチャの場合、オーバーフローでトラップが発生する可能性が非常に高くなります(グラフィックスアプリケーションについてどのように話すかを見ると、このような奇妙なアーキテクチャである可能性はかなり低いです)。移植性は問題ですか?もしそうなら、あなたは棄権したいと思うかもしれません。

最後に、コンパイラ/オプティマイザ側があります。オーバーフローが未定義である理由の1つは、単にそれをそのままにしておくのが、かつてのハードウェアに対処するのが最も簡単だったことです。しかし、もう1つの理由は、たとえばx+1が常により大きいことが保証されてxおり、コンパイラ/オプティマイザがこの知識を利用できるためです。さて、前述のケースでは、コンパイラは実際にこのように動作し、単に完全なブロックを取り除くことが知られています(正確にこれが原因でコンパイラがいくつかの検証コードを完全に取り除いたことに基づいた、数年前のLinuxエクスプロイトが存在していました)。
あなたの場合、コンパイラが特別で奇妙な最適化を行うことを真剣に疑っています。しかし、あなたは何を知っていますか、私は何を知っていますか。疑問がある場合は、試してください。うまくいけば、あなたは行ってもいいです。

(そして最後に、もちろんコード監査があります。運が悪ければ、監査人とこれについて話し合うのに時間を浪費する必要があるかもしれません。)

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