算術オーバーフローが無視されるのはなぜですか?


76

お気に入りのプログラミング言語で1〜2,000,000のすべての数値を合計しようとしたことがありますか?結果は手動で簡単に計算できます。2,000,001,000,000は、符号なし32ビット整数の最大値の約900倍です。

C#が出力される-1453759936-負の値!そして、Javaも同じことをしていると思います。

つまり、デフォルトでは算術オーバーフローを無視する一般的なプログラミング言語がいくつかあります(C#には、それを変更するための非表示オプションがあります)。それは私には非常に危険に見える動作であり、Ariane 5のクラッシュはそのようなオーバーフローによって引き起こされたのではありませんか?

では、このような危険な動作の背後にある設計上の決定は何ですか?

編集:

この質問に対する最初の回答は、チェックの過剰なコストを表しています。短いC#プログラムを実行して、この仮定をテストしましょう。

Stopwatch watch = Stopwatch.StartNew();
checked
{
    for (int i = 0; i < 200000; i++)
    {
        int sum = 0;
        for (int j = 1; j < 50000; j++)
        {
            sum += j;
        }
    }
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);

私のマシンでは、チェックされていないバージョンは4125msかかりますが、チェックされたバージョンは11015msかかります。つまり、チェック手順は、数値を追加するのにほぼ2倍の時間がかかります(元の時間の合計3倍)。しかし、10,000,000,000回の繰り返しで、チェックにかかる時間はまだ1ナノ秒未満です。それが重要な状況があるかもしれませんが、ほとんどのアプリケーションでは、それは重要ではありません。

編集2:

サーバーアプリケーション(いくつかのセンサーから受信したデータを分析するWindowsサービス)を/p:CheckForOverflowUnderflow="false"パラメーターで再コンパイルし(通常、オーバーフローチェックをオンにします)、デバイスに展開しました。Nagiosモニタリングは、平均CPU負荷が17%に留まったことを示しています。

これは、上記の構成例で見つかったパフォーマンスヒットは、アプリケーションにとってまったく無関係であることを意味します。


19
メモとして、C#の場合、checked { }セクションを使用して、算術オーバーフローチェックを実行する必要があるコードの部分をマークできます。これはパフォーマンスによるものです
パウェウチュカシク

14
「お気に入りのプログラミング言語で1から2,000,000までのすべての数値を合計しようとしたことはありますか?」–はい:(1..2_000_000).sum #=> 2000001000000。私のお気に入りの言語のもう1つ:sum [1 .. 2000000] --=> 2000001000000。私のお気に入りではありませんArray.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000。(公平を期すために、最後のものは不正行為です。)
ヨルグWミットタグ

27
IntegerHaskellの@BernhardHiller は任意精度であり、割り当て可能なRAMが不足しない限り、任意の数値を保持します。
ポリノーム

50
Ariane 5のクラッシュは、問題ではないオーバーフローをチェックすることで発生しました。ロケットは、計算の結果が必要でなくなったフライトの一部にありました。代わりに、オーバーフローが検出されたため、フライトが中止されました。
サイモンB

9
But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.これは、ループが最適化されていないことを示しています。また、その文は、私にとって非常に有効な以前の数字と矛盾しています。
usr

回答:


86

これには3つの理由があります。

  1. 実行時にオーバーフローを(すべての算術演算ごとに)チェックするコストは過剰です。

  2. オーバーフローチェックをコンパイル時に省略できることを証明するのは非常に複雑です。

  3. 場合によっては(CRC計算、多数のライブラリなど)、「オーバーフローでラップ」がプログラマにとってより便利です。


10
@DmitryGrigoryev unsigned intは、オーバーフローチェックのある言語ではデフォルトですべての整数型をチェックする必要があるため、気にする必要はありません。書く必要がありますwrapping unsigned int
user253751

32
私はコストの議論を買いません。CPUは、すべての整数計算でオーバーフローをチェックし、ALUにキャリーフラグを設定します。欠けているのはプログラミング言語のサポートです。キャリーフラグへのアクセスを許可する単純なdidOverflow()インライン関数やグローバル変数__carryを使用しない場合、CPU時間はゼロになります。
スリーブマン

37
@slebetman:それはx86です。ARMはサポートしていません。たとえばADD、キャリーを設定しません(必要ですADDS)。Itaniumにはキャリーフラグさえありません。また、x86でも、AVXにはキャリーフラグがありません。
-MSalters

30
@slebetmanキャリーフラグを設定します、はい(x86では気になります)。しかし、その後、キャリーフラグを読み取って結果を決定する必要があります。これは高価な部分です。算術演算はループで頻繁に使用されるため(その場合はタイトなループ)、追加の命令が1つだけ必要な場合でもパフォーマンスに非常に大きな影響を及ぼす可能性のある多くの安全なコンパイラー最適化を簡単に防ぐことができます)。デフォルトにする必要があるということですか?多分、特にC#のような言語でuncheckedは、言うのが十分に簡単です。ただし、オーバーフローが重要になる頻度を過大評価している可能性があります。
ルアーン

12
ARMのadds価格は同じですadd(キャリーフラグを更新するかどうかを選択する命令1ビットフラグです)。MIPSのadd命令はオーバーフロー時にトラップします- 代わりに使用して、オーバーフロー時にトラップしないように要求する必要がありますaddu
user253751

65

それは悪いトレードオフだと誰が言いますか?!

オーバーフローチェックを有効にして、すべての運用アプリを実行します。これはC#コンパイラオプションです。実際にこれをベンチマークしましたが、違いを判別できませんでした。(おもちゃではない)HTMLを生成するためにデータベースにアクセスするコストは、オーバーフローチェックのコストを覆い隠します。

本番環境ではオペレーションがオーバーフローしないことを知っているという事実に感謝しています。ほとんどすべてのコードは、オーバーフローが発生すると不安定に動作します。バグは害がありません。データ破損が発生する可能性が高く、セキュリティが問題になる可能性があります。

パフォーマンスが必要な場合(場合によってはそうです)unchecked {}、きめ細かい基準でオーバーフローチェックを無効にします。オーバーフローしない操作に依存していると呼びたい場合checked {}、その事実を文書化するためにコードに重複して追加することがあります。私はオーバーフローを意識していますが、必ずしもチェックのおかげである必要はありません。

C#チームは、デフォルトでオーバーフローをチェックしないことを選択したときに間違った選択をしたと思いますが、互換性に関する強い懸念により、その選択は今では封印されています。この選択は2000年ごろに行われたことに注意してください。ハードウェアの能力は低く、.NETにはまだ多くの牽引力がありませんでした。.NETは、この方法でJavaおよびC / C ++プログラマにアピールしたかったかもしれません。.NETは、金属に近づけることもできます。そのため、Javaにはない安全でないコード、構造体、および優れたネイティブ呼び出し機能があります。

ハードウェアが高速になり、アウトコンパイラが賢くなるほど、デフォルトでより魅力的なオーバーフローチェックが行われます。

また、オーバーフローチェックは、無限サイズの数値よりも優れていることが多いと考えています。無限のサイズの数値は、パフォーマンスコストがさらに高く、最適化が難しく(信じている)、無制限のリソース消費の可能性を開きます。

JavaScriptのオーバーフロー処理方法はさらに悪いです。JavaScriptの数値は浮動小数点の倍精度です。「オーバーフロー」は、整数の完全に正確なセットを残すものとして現れます。少し間違った結果が発生します(1つずれているなど-有限ループを無限ループに変えることができます)。

C / C ++などの一部の言語では、これらの言語で記述されている種類のアプリケーションにはベアメタルのパフォーマンスが必要なため、デフォルトでのオーバーフローチェックは明らかに不適切です。それでも、C / C ++をより安全なモードにオプトインできるようにすることで、より安全な言語にする努力があります。コードの90〜99%は寒い傾向があるため、これは賞賛に値します。例は、fwrapv2の補数ラップを強制するコンパイラオプションです。これは、言語ではなく、コンパイラによる「実装の品質」機能です。

Haskellには論理呼び出しスタックも評価順序も指定されていません。これにより、予測不可能なポイントで例外が発生します。でa + b、それかどうかを指定されていないaか、b第一およびそれらの式がまったく終了したか否かを評価します。したがって、Haskellがほとんどの場合、無制限の整数を使用することは理にかなっています。ほとんどのHaskellコードでは例外は本当に不適切であるため、この選択は純粋に機能的な言語に適しています。そして、ゼロによる除算は実際、Haskells言語設計の問題点です。制限のない整数の代わりに、固定幅のラッピング整数を使用することもできましたが、それは言語の特徴である「正確性に焦点を当てる」テーマには合いません。

オーバーフロー例外に代わるものは、未定義の操作によって作成され、操作を介して伝播する有害NaN値(フロート値など)です。これは、オーバーフローチェックよりもはるかに高価であり、失敗する可能性がある操作だけでなく、すべての操作を遅くします(フロートが一般的に持ち、intが一般に持っていないハードウェアアクセラレーションを除きます-Itaniumには "Not a Thing"であるNaTがあります)。また、プログラムに不良データと一緒にリンプを継続させるという点もよくわかりません。のようなものON ERROR RESUME NEXTです。エラーを隠しますが、正しい結果を得るのに役立ちません。supercatは、これを行うことがパフォーマンスの最適化である場合があることを指摘しています。


2
素晴らしい答え。では、彼らがそのように決めた理由についてのあなたの理論は何ですか?Cをコピーし、最終的にはアセンブリとバイナリをコピーした他の全員をコピーするだけですか?
jpmc26

19
ユーザーベースの99%が行動を期待しているとき、あなたはそれを彼らに与える傾向があります。「Cのコピー」に関しては、実際にはCのコピーではなく、Cの拡張です。Cは、unsigned整数に対してのみ例外のない動作を保証します。符号付き整数オーバーフローの動作は、実際にはCおよびC ++での未定義の動作です。はい、未定義の動作です。ほとんどの人が2の補数のオーバーフローとして実装するのはまさにそのためです。C#が、実際にそれが公式ではなく、UB C / C ++のようなことを残します
Cortのアモン

10
@CortAmmon:デニスリッチーが設計した言語は、符号付き整数のラップアラウンド動作を定義していましたが、2の補数でないプラットフォームでの使用にはあまり適していませんでした。正確な2の補数のラップアラウンドからの一定の逸脱を許可すると、いくつかの最適化を大幅に支援できます(たとえば、コンパイラーがx * y / yをxに置き換えて乗算と除算を節約できるようにする)一方で、コンパイラーの作家は未定義の動作を与えられたターゲットプラットフォームとアプリケーションフィールドにとって意味のあるものですが、むしろ窓から感覚を捨てる機会として。
-supercat

3
@CortAmmon -によって生成されたコードチェックgcc -O2のためにx + 1 > x(ここxint)。gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/…も参照してください。Cの符号付きオーバーフローでの2の補数動作は、実際のコンパイラでもオプションでありgcc、通常の最適化レベルではデフォルトで無視されます。
ジョナサンキャスト

2
@supercatええ、ほとんどのCコンパイラライターは、プログラマーに合理的なセマンティクスを提供しようとするよりも、非現実的なベンチマークを0.5%速く実行することに関心があります(はい、なぜ解決するのが簡単な問題ではなく、組み合わされたときの予期しない結果、やだ、やだ、それでもまだ焦点が合っていないので、会話に従うと気づきます)。幸いなことに、もっとうまくやろうとする人がいます
Voo

30

オーバーフロー発生するというまれなケースを自動的にキャッチするために、すべての計算をはるかに高価にすることは悪いトレードオフだからです。すべてのプログラマーが使用しない機能の代価を支払うよりも、これが問題であるまれなケースを認識して特別な予防策を追加することで、プログラマーに負担をかける方がはるかに優れています。


28
それは何とか彼らはめったに発生しないので、バッファオーバーフローのためのチェックが省略されるべきであると言ってようなものだ...
ベルンハルト・ヒラー

73
@BernhardHiller:それがまさにCとC ++が行うことです。
マイケルボルグワード

12
@DavidBrown:算術オーバーフローも同様です。ただし、前者はVMを侵害しません。

35
@Deduplicatorは素晴らしい点です。CLRは、検証可能なプログラムが、悪いことが発生した場合でもランタイムの不変条件に違反できないように、慎重に設計されました。もちろん、安全なプログラムは、悪いことが起こったときに、自身の不変式に違反する可能性があります。
エリックリッパー

7
@svick算術演算は、おそらく配列のインデックス演算よりもはるかに一般的です。また、ほとんどの整数サイズは十分に大きいため、オーバーフローする演算を実行することは非常にまれです。したがって、費用便益比は非常に異なります。
バーマー

20

このような危険な動作の背後にある設計上の決定は何ですか?

「ユーザーが必要としない機能に対してパフォーマンスのペナルティを支払うことを強制しないでください。」

これは、CおよびC ++の設計における最も基本的な教義の1つであり、今日では些細なことと見なされているタスクに対してほとんど適切なパフォーマンスを得るためにとんでもないゆがみを経験しなければならなかった別の時代に由来します。

新しい言語は、配列の境界チェックなど、他の多くの機能に対するこの姿勢を破ります。なぜ彼らがオーバーフローチェックのためにそれをしなかったのか分かりません。それは単に見落としかもしれません。


18
C#の設計の見落としではありません。C#の設計者は、2つのモードを意図的に作成しました。checkedとをuncheckedローカルで切り替えるための構文と、グローバルに変更するコマンドラインスイッチ(およびVSのプロジェクト設定)を追加しました。uncheckedデフォルトを作成することに同意しないかもしれません(私はそうします)が、これは明らかに非常に慎重です。
-svick

8
@slebetman-記録のためだけです:ここでのコストは、オーバーフローをチェックするコスト(些細なこと)ではなく、オーバーフローが発生したかどうかに応じて異なるコードを実行するコスト(非常に高価です)です。CPUは条件分岐ステートメントを好みません。
ジョナサンキャスト

5
@jcast最新のプロセッサでの分岐予測は、条件付き分岐ステートメントのペナルティをほぼ解消しませんか?正常な場合はすべてオーバーフローしないはずなので、非常に予測可能な分岐動作です。
-CodeMonkey

4
@CodeMonkeyに同意します。コンパイラーは、通常はロード/コールドされていないページへのオーバーフローの場合に条件付きジャンプを行います。そのためのデフォルトの予測は「行われません」であり、おそらく変更されません。総オーバーヘッドは、パイプラインの1つの命令です。しかし、それは算術命令ごとに1つの命令オーバーヘッドです。
–MSalters

2
@MSaltersはい、追加の命令オーバーヘッドがあります。CPUに限定された問題がある場合、影響は大きくなる可能性があります。IOとCPUの重いコードが混在するほとんどのアプリケーションでは、影響は最小限であると思います。デバッグビルドでのみオーバーヘッドを追加し、リリースビルドではオーバーヘッドを削除するRustの方法が気に入っています。
-CodeMonkey

20

レガシー

この問題はおそらくレガシーに根ざしていると思います。Cで:

  • 符号付きオーバーフローは未定義の動作です(コンパイラはラップするフラグをサポートしています)。
  • 符号なしオーバーフローは、定義された動作です(ラップします)。

これは、プログラマが何をしているのかをプログラマが知っているという原則に従って、最高のパフォーマンスを得るために行われました。

Statu-Quoにつながる

C(および拡張C ++による)が順番にオーバーフローを検出する必要がないという事実は、オーバーフローチェックが遅いことを意味します。

ハードウェアは主にC / C ++に対応しています(真剣に、x86にはstrcmp命令(SSE 4.2ではPCMPISTRIとも呼ばれます!)があります)。Cは気にしないので、一般的なCPUはオーバーフローを検出する効率的な方法を提供しません。x86では、潜在的にオーバーフローする各操作の後に、コアごとのフラグをチェックする必要があります。本当に欲しいのは、結果に「汚染された」フラグがある場合です(NaNが伝播するのと同じです)。また、ベクトル演算にはさらに問題があります。一部の新しいプレーヤーは、効率的なオーバーフロー処理で市場に登場します。しかし、今のところx86とARMは気にしません。

コンパイラーオプティマイザーは、オーバーフローチェックの最適化、またはオーバーフローの存在下での最適化も得意ではありません。ジョン・レガーなどの一部の学者は、この状況に文句を言っていますが、実際には、オーバーフローが発生するという単純な事実が「失敗」すると、アセンブリがCPUにヒットする前でも最適化が妨げられることがあります。特に自動ベクトル化を防ぐ場合...

カスケード効果あり

したがって、効率的な最適化戦略と効率的なCPUサポートがない場合、オーバーフローチェックにはコストがかかります。ラッピングよりもはるかに高価です。

いくつかの迷惑な動作を追加します。たとえばx + y - 1x - 1 + yそうでない場合はオーバーフローする可能性があり、正当にユーザーを困らせる可能性があります。また、オーバーフローチェックは通常、ラッピングを優先して破棄されます(この例および他の多くの例を適切に処理します)。

それでも、すべての希望が失われるわけではありません

clangおよびgccコンパイラーでは、「サニタイザー」を実装する努力が行われています。つまり、未定義の動作のケースを検出するためにバイナリを計測する方法です。を使用すると-fsanitize=undefined、符号付きオーバーフローが検出され、プログラムが中止されます。テスト中に非常に便利です。

錆のプログラミング言語は、オーバーフローチェック有効になっているデフォルトでは(それがパフォーマンス上の理由からリリースモードでラッピング算術を使用しています)デバッグモードでは。

そのため、オーバーフローチェックと偽の結果が検出されない危険性に対する懸念が高まっており、これが研究コミュニティ、コンパイラコミュニティ、ハードウェアコミュニティへの関心を喚起することを願っています。


6
オーバーフローをチェックするための効果的な方法の反対だ@DmitryGrigoryev、ハスウェルに例えば、それは、サイクルごとに4回の通常の加算からわずか1追加をチェックし、それはの分岐予測ミスの影響考慮前だにスループットが低下jo年代を、そして汚染のよりグローバルな影響は、分岐予測子の状態とコードサイズの増加に追加します。そのフラグがスティッキーである場合、それは本当の可能性を提供します。そして、それでもベクトル化されたコードでは適切にそれを行うことができません。

3
あなたはJohn Regehrによって書かれたブログ投稿にリンクしているので、リンクした記事の数ヶ月前に書かれた彼の記事の別の記事にもリンクすることが適切だと思いました。これらの記事では、さまざまな哲学について説明しています。以前の記事では、整数のサイズは固定されていました。整数演算がチェックされます(つまり、コードは実行を継続できません)。例外またはトラップがあります。新しい記事では、固定サイズの整数を完全に捨てることで、オーバーフローを排除する方法について説明しています。
-rwong

2
@rwong無限サイズの整数にも問題があります。オーバーフローがバグ(多くの場合)の結果である場合、すべてがひどく失敗するまで、すべてのサーバーリソースを消費する長時間の苦痛にクイッククラッシュを変える可能性があります。私は主に「早期失敗」アプローチのファンです-環境全体を汚染する可能性が低くなります。私は1..100代わりにPascal風の型を好む-2 ^ 31などに「強制」されるのではなく、期待される範囲について明示的にしてください。もちろん、一部の言語はこれを提供し、デフォルトでオーバーフローチェックを行う傾向があります(時にはコンパイル時でも)。
ルアーン

1
@Luaan:興味深いのは、多くの場合、中間計算が一時的にオーバーフローすることがありますが、結果はオーバーフローしないことです。たとえば、1..100の範囲でx * 2 - 2x、結果が収まる場合でも51のときにオーバーフローする可能性があり、計算を再配置する必要があります(不自然な方法もあります)。私の経験では、通常、より大きなタイプで計算を実行し、結果が適合するかどうかを確認することを好みます。
マチューM.

1
なつみ ええ、それが「十分にスマートなコンパイラー」の領域に入る場所です。理想的には、真の1..100が期待されるコンテキストで決して使用されない限り、103の値は1..100タイプに対して有効である必要があります(たとえば、割り当てが有効な1になるx = x * 2 - 2すべてに対して機能するはずxです)。 .100番号)。つまり、数値型に対する操作は、割り当てが適合する限り、型自体よりも高い精度を持つ場合があります。これは(a + b) / 2、(符号なしの)オーバーフローを無視することが正しいオプションであるような場合に非常に役立ちます。
ルアーン

10

オーバーフローを検出しようとする言語は、そうでなければ有用な最適化となるものを厳しく制限する方法で、関連するセマンティクスを歴史的に定義してきました。とりわけ、コードで指定されたものとは異なるシーケンスで計算を実行することがしばしば有用ですが、オーバーフローをトラップするほとんどの言語は、次のようなコードを保証します:

for (int i=0; i<100; i++)
{
  Operation1();
  x+=i;
  Operation2();
}

xの開始値がループの47番目のパスでオーバーフローを引き起こす場合、Operation1は47回実行され、Operation2は46を実行します。このような保証がない場合、ループ内でxを使用するものがなければ、 Operation1またはOperation2によってスローされた例外の後にxの値を使用します。コードは次のように置き換えることができます。

x+=4950;
for (int i=0; i<100; i++)
{
  Operation1();
  Operation2();
}

残念ながら、ループ内でオーバーフローが発生した場合に正しいセマンティクスを保証しながらそのような最適化を実行することは困難です。

if (x < INT_MAX-4950)
{
  x+=4950;
  for (int i=0; i<100; i++)
  {
    Operation1();
    Operation2();
  }
}
else
{
  for (int i=0; i<100; i++)
  {
    Operation1();
    x+=i;
    Operation2();
  }
}

多くの現実世界のコードがより複雑なループを使用していると考えると、オーバーフローのセマンティクスを維持しながらコードを最適化することが難しいことは明らかです。さらに、キャッシュの問題のため、一般的に実行されるパスでの操作が少ない場合でも、コードサイズの増加によりプログラム全体の実行が遅くなる可能性があります。

オーバーフロー検出を安価にするために必要なのは、結果に影響する可能性のあるオーバーフローなしで計算が実行されたかどうかをコードが簡単に報告できるようにする、より緩やかなオーバーフロー検出セマンティクスの定義されたセットです*それ以上の詳細を持つコンパイラ。言語仕様が、オーバーフロー検出のコストを上記の実現に必要な最小限に削減することに焦点を合わせている場合、既存の言語よりもはるかに低コストにすることができます。ただし、効率的なオーバーフロー検出を容易にするための努力は知りません。

(*)すべてのオーバーフローが報告されることを言語が約束している場合、オーバーフローx*y/yxないx*yことが保証されない限り、次のような式は単純化できません。同様に、計算の結果が無視される場合でも、すべてのオーバーフローを報告することを約束する言語は、オーバーフローチェックを実行できるように、とにかくそれを実行する必要があります。そのような場合のオーバーフローは算術的に不正確な振る舞いにはならないため、プログラムはそのようなチェックを実行してオーバーフローが不正確な結果を引き起こさないことを保証する必要はありません。

ちなみに、Cでのオーバーフローは特に悪いです。C99をサポートするほとんどすべてのハードウェアプラットフォームは2の補数のサイレントラップアラウンドセマンティクスを使用しますが、現代のコンパイラーでは、オーバーフローの場合に任意の副作用を引き起こす可能性のあるコードを生成できます。たとえば、次のようなものが与えられた場合:

#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
  uint32_t total=0;
  q|=32768;
  for (int i = 32768; i<=q; i++)
  {
    total+=test(i,65535);
    *p+=1;
  }
  return total;
}

GCCはtest2のコードを生成し、qに渡される値に関係なく、無条件に1回インクリメント(* p)し、32768を返します。その推論により、(32769 * 65535)&65535uの計算はオーバーフローを引き起こすため、コンパイラーは(q | 32768)が32768より大きい値を生成する場合を考慮する必要はありません。 (32769 * 65535)と65535uの計算が結果の上位ビットを考慮する必要があるため、gccはループを無視する正当化として符号付きオーバーフローを使用します。


2
「現代のコンパイラーにとってはファッショナブルです...」-同様に、特定の有名なカーネルの開発者が、使用した最適化フラグに関するドキュメントを読まずにインターネット全体で怒りを発することを選択することも簡単にファッショナブルでした彼らが望む動作を得るために、さらに多くのコンパイラフラグを追加することを余儀なくされたためです;-)。この場合、-fwrapv質問者が望んでいる動作ではありませんが、定義された動作になります。確かに、gcc最適化は、あらゆる種類のC開発を、標準およびコンパイラーの動作に関する徹底的な試験に変えます。
スティーブジェソップ

1
@SteveJessop:「未定義の動作」が「基礎となるプラットフォームで意味のあることをする」ことを意味する低レベルの方言をコンパイラ作成者が認識し、それによって暗示される不必要な保証を放棄する方法をプログラマーが追加した場合、Cはより健全な言語になります。規格内の「移植性のない、または誤りのある」というフレーズは、単に「誤った」という意味だと想定するのではありません。多くの場合、動作保証が弱い言語で取得できる最適なコードは、保証が強い場合や保証がない場合よりもはるかに優れています。たとえば
...-supercat

1
...プログラマがx+y > zyield 0またはyield 1以外のことを一切行わない方法で評価する必要があるが、オーバーフローの場合にどちらの結果も等しく受け入れられる場合、その保証を提供するコンパイラは、多くの場合、より良いコードを生成できます式はx+y > z、どのコンパイラよりも防御的に記述された式のバージョンを生成できます。現実的に言えば、有用なオーバーフロー関連の最適化のどの部分が、除算/剰余以外の整数計算が副作用なしで実行されるという保証によって排除されるでしょうか?
-supercat

私は詳細に完全には触れていないことを告白しますが、あなたのudgeみは一般に「コンパイラライター」にあり、具体的には「私の-fwhatever-makes-senseパッチを受け入れないgccの人」ではないという事実は、もっとあることを強く示唆していますそれに気まぐれよりも彼らに。私が聞いた通常の議論は、コードのインライン化(​​およびマクロ展開さえ)は、コード構成の特定の使用について可能な限り推論することから利益を得るということです。に、周囲のコードが不可能を「証明」すること。
スティーブジェソップ

簡単な例として、私が書いた場合foo(i + INT_MAX + 1)、コンパイラーライターはfoo()、引数が非負であることに依存するインラインコードに最適化を適用することに熱心です(おそらく、悪意のあるdivmodトリック)。追加の制限の下では、負の入力に対する動作がプラットフォームにとって意味のある最適化のみを適用できます。もちろん、個人的には、それを-fオンにする-fwrapvなどのオプションであり、フラグのない最適化を無効にする必要があることを嬉しく思います。しかし、それは私が自分で仕事をするすべてを気にすることができるというわけではありません。
スティーブジェソップ

9

すべてのプログラミング言語が整数オーバーフローを無視するわけではありません。一部の言語はすべての数値(ほとんどのLisp方言、Ruby、Smalltalkなど)に対して安全な整数演算を提供し、他の言語はライブラリを介して提供します。たとえば、C ++にはさまざまなBigIntクラスがあります。

言語がデフォルトで整数をオーバーフローから安全にするかどうかは、その目的に依存します。CやC ++などのシステム言語は、ゼロコストの抽象化を提供する必要があり、「大きな整数」は1ではありません。Rubyなどの生産性言語は、そのままで大きな整数を提供できます。JavaやC#などの中間の言語は、私見では安全な整数をそのまま使用すべきですが、そうではありません。


オーバーフローの検出(およびシグナル、パニック、例外など)の検出と、大きな数値への切り替えには違いがあることに注意してください。前者は後者よりもはるかに安く実行できるはずです。
マチューM.

なつみ 絶対に-そして、私は私の答えでそれについて明確ではないことを理解しています。
ネマンジャトリフノビッチ

7

あなたが示したように、デフォルトでオーバーフローチェックが有効になっている場合、C#は3倍遅くなります(あなたの例はその言語の典型的なアプリケーションであると仮定して)。パフォーマンスが常に最も重要な機能ではないことに同意しますが、言語/コンパイラは通常、典型的なタスクでのパフォーマンスで比較されます。これは、パフォーマンステストが客観的である一方で、言語機能の品質がある程度主観的であるという事実に一部起因します。

ほとんどの点でC#に似ているが3倍遅い新しい言語を導入する場合、最終的にほとんどのエンドユーザーがオーバーフローチェックの恩恵を受ける場合でも、市場シェアを獲得することは容易ではありません。より高いパフォーマンスから。


10
これは特に、開発が困難な開発者の生産性指標や、セキュリティ違反からのキャッシュ保存の指標ではなく、JavaとC ++と比較して初期のC#の場合に当てはまりました。これは測定が困難ですが、簡単なパフォーマンスベンチマークに基づいています。
エリックリッパー

1
おそらく、CPUのパフォーマンスは、いくつかの単純な数値計算でチェックされます。したがって、オーバーフロー検出の最適化により、これらのテストで「悪い」結果が生じる可能性があります。キャッチ22。
ベルンハルトヒラー

5

パフォーマンスに基づくオーバーフローチェックの欠如を正当化する多くの答えに加えて、考慮すべき2種類の算術があります。

  1. インデックス計算(配列のインデックス付けやポインタ演算)

  2. その他の算術

言語がポインターサイズと同じ整数サイズを使用する場合、適切に構築されたプログラムは、インデックス計算がオーバーフローを引き起こす前に必ずメモリを使い果たす必要があるため、インデックス計算を行うことでオーバーフローしません。

したがって、割り当てられたデータ構造に関係するポインター演算およびインデックス式で作業する場合は、メモリ割り当てのチェックで十分です。たとえば、32ビットのアドレス空間があり、32ビットの整数を使用し、最大2GBのヒープを割り当てることができる場合(アドレス空間の約半分)、インデックス/ポインターの計算(基本的に)はオーバーフローしません。

さらに、加算/減算/乗算のどれだけが配列のインデックス付けまたはポインター計算に関係するかについて驚くかもしれません。したがって、最初のカテゴリーに分類されます。オブジェクトポインター、フィールドアクセス、および配列操作はインデックス作成操作であり、多くのプログラムはこれら以上の算術計算を行いません! 基本的に、これがプログラムが整数オーバーフローチェックなしで動作するのと同様に動作する主な理由です。

すべての非インデックスおよび非ポインター計算は、オーバーフローを必要とする/期待するもの(ハッシュ計算など)とそうでないもの(例:集計例)に分類する必要があります。

後者の場合、プログラマは多くの場合、doubleまたはsome などの代替データ型を使用しますBigInt。多くの計算では、財務計算などdecimalではなく、データ型が必要ですdouble。そうでなく、整数型に固執する場合は、整数オーバーフローをチェックする必要があります。そうでなければ、指摘しているように、プログラムは検出されないエラー状態に達する可能性があります。

プログラマーとして、精度は言うまでもなく、オーバーフローの可能性に関して、数値データ型の選択とそれらの結果に敏感である必要があります。一般的に(そして特に高速整数型を使用したいC言語ファミリを使用する場合)、インデックス計算と他の計算の違いに敏感であり、認識する必要があります。


3

Rust言語は、デバッグビルドのチェックを追加し、最適化されたリリースバージョンでそれらを削除することにより、オーバーフローをチェックすることとしないことの間の興味深い妥協点を提供します。これにより、テスト中にバグを見つけることができますが、最終バージョンでは完全なパフォーマンスが得られます。

オーバーフローラップアラウンドは動作を必要とすることがあるため、オーバーフローをチェックしないバージョンの演算子もあります。

変更のRFCでの選択の背後にある理由について詳しく読むことができます。また、このブログ投稿には、この機能がキャッチに役立ったバグのリストなど、多くの興味深い情報があります。


2
Rustは、などのメソッドも提供します。このメソッドはchecked_mul、オーバーフローが発生したかどうかを確認し、発生したNone場合は返しますSome。これは、実稼働モードおよびデバッグモードで使用できます。doc.rust
lang.org

3

Swiftでは、デフォルトで整数オーバーフローが検出され、プログラムが即座に停止します。ラップアラウンド動作が必要な場合、それを実現するさまざまな演算子&+、&-、および&*があります。そして、操作を実行してオーバーフローがあったかどうかを伝える関数があります。

初心者がCollat​​zシーケンスを評価し、コードがクラッシュするのを見るのは楽しいです:-)

現在、Swiftの設計者はLLVMとClangの設計者でもあるため、最適化について少し知識があり、不要なオーバーフローチェックを回避することができます。すべての最適化を有効にすると、オーバーフローチェックはコードサイズと実行時間にあまり影響しません。また、ほとんどのオーバーフローは絶対に不正確な結果につながるため、コードサイズと実行時間が適切に費やされます。

PS。C、C ++では、Objective-Cの符号付き整数演算オーバーフローは未定義の動作です。つまり、符号付き整数オーバーフローの場合にコンパイラが行うことは、定義により正しいことを意味します。符号付き整数のオーバーフローに対処する一般的な方法は、CPUが提供する結果をすべて無視し、そのようなオーバーフローが発生しないという仮定をコンパイラーに構築することです(たとえば、n + 1> nは常にtrueであると結論付けます)また、Swiftのようにオーバーフローが発生した場合はチェックしてクラッシュすることはほとんどありません。


1
CでUB主導の狂気を押し進めている人々が、他の言語を支持してひそかにそれを弱体化しようとしているのではないかと私は時々疑問に思いました。それは理にかなっています。
-supercat

x+1>xコンパイラが任意のより大きな型を使用して整数式を便利に評価できる場合(またはそうするように振る舞う場合)、コンパイラがxについて「仮定」を行うことを無条件に真として扱う必要はありません。オーバーフローベースの「仮定」の厄介な例はuint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }、コンパイラが32768を超えないことsum += mul(65535, x)を決定するために使用できることを決定するxことです[C89理論を書いた人々に衝撃を与える可能性のある動作。 ..
スーパーキャット

...にunsigned short昇格させるのsigned intは、2の補数のサイレントラップアラウンド実装(つまり、使用中のC実装の大半)がunsigned shortintまたはに昇格されるかどうかにかかわらず、上記のようなコードを同じように扱うという事実でしたunsigned。上記のようなコードを正常に処理するために、標準はサイレントラップアラウンド2の補数ハードウェアでの実装を必要としませんでした、標準の作成者は、そうすることを期待していたようです。
-supercat

2

実際、これの本当の原因は純粋に技術的/歴史的です:CPUの大部分の無視記号。通常、レジスタに2つの整数を追加する命令は1つだけであり、CPUはこれら2つの整数を符号付きまたは符号なしとして解釈するかどうかに少しも気にしません。同じことが減算にも、乗算にも当てはまります。符号を認識する必要がある唯一の算術演算は除算です。

これが機能する理由は、事実上すべてのCPUで使用される符号付き整数の2の補数表現です。たとえば、4ビットの2の補数では、5と-3の追加は次のようになります。

  0101   (5)
  1101   (-3)
(11010)  (carry)
  ----
  0010   (2)

キャリーアウトビットを捨てるラップアラウンド動作が正しい署名結果をどのようにもたらすかを観察します。同様に、CPUは通常、減算x - yx + ~y + 1次のように実装します。

  0101   (5)
  1100   (~3, binary negation!)
(11011)  (carry, we carry in a 1 bit!)
  ----
  0010   (2)

これは、減算をハードウェアの加算として実装し、簡単な方法で算術論理ユニット(ALU)への入力のみを微調整します。もっとシンプルなものはありますか?

乗算は加算のシーケンスにすぎないため、同様に適切に動作します。2の補数表現を使用し、算術演算のキャリーアウトを無視すると、回路が簡素化され、命令セットが簡素化されます。

明らかに、Cは金属の近くで動作するように設計されているため、符号なし算術の標準化された動作とまったく同じ動作を採用し、符号付き演算のみで未定義の動作を実現できます。そして、その選択はJavaのような他の言語、そして明らかにC#にも引き継がれました。


私もこの答えをするためにここに来ました。
ミスターリスター

残念ながら、一部の人々は、プラットフォームで低レベルのCコードを記述する人々が、そのような目的に適したCコンパイラがオーバーフローの場合に制約された方法で動作することを期待する大胆さを持つべきであるという概念をひどく不合理であると見なしているようです。個人的には、コンパイラがコンパイラの都合で任意に拡張された精度を使用して実行されるかのようにコンパイラが動作するのは合理的だと思います(したがって、32ビットシステムでは、の場合x==INT_MAXx+1コンパイラで+2147483648または-2147483648のいずれかとして任意に動作する可能性があります利便性)、しかし
...-supercat

一部の人は、以下の場合にすることを考えているようだxyしているuint16_tと、32ビットシステム上でコードを計算しx*y & 65535uたときにyときに到達することはありません65535で、コンパイラは、そのコードを想定する必要がありx32768以上である
supercat

1

確認のコストについていくつかの回答がありましたが、これが合理的な正当性であることを争うために回答を編集しました。これらのポイントに対処しようとします。

CおよびC ++では(例として)、言語設計の原則の1つは、要求されていない機能を提供しないことです。これは一般に、「使用しないものに料金を支払わない」というフレーズで要約されます。プログラマーがオーバーフローチェックを希望する場合、プログラマーはそれを要求できます(そしてペナルティーを支払います)。これにより、言語の使用がより危険になりますが、それを知っている言語で作業することを選択するため、リスクを受け入れます。そのリスクが望ましくない場合、または安全性が最高のパフォーマンスであるコードを記述している場合、パフォーマンス/リスクのトレードオフが異なるより適切な言語を選択できます。

しかし、10,000,000,000回の繰り返しで、チェックにかかる時間はまだ1ナノ秒未満です。

この推論にはいくつか間違った点があります。

  1. これは環境固有です。このような特定の図を引用することは一般にほとんど意味がありません。コードは、パフォーマンスの点で桁違いに変化するあらゆる種類の環境向けに記述されているためです。デスクトップコンピューターでの1ナノ秒は、組み込み環境向けにコーディングしている人には驚くほど速いように見え、スーパーコンピュータークラスター向けにコーディングしている人には耐えられないほど遅いかもしれません。

  2. 1ナノ秒は、まれにしか実行されないコードセグメントにとっては何のようにも見えないかもしれません。一方、コードの主な機能である計算の内側のループにある場合、削ることができる時間のすべての部分が大きな違いを生む可能性があります。クラスターでシミュレーションを実行している場合、内部ループでナノ秒の何分の1かの節約された部分は、ハードウェアと電気に費やされたお金に直接変換できます。

  3. 一部のアルゴリズムとコンテキストでは、10,000,000,000回の反復は重要ではありません。繰り返しますが、特定のコンテキストにのみ適用される特定のシナリオについて話すことは一般的に意味がありません。

それが重要な状況があるかもしれませんが、ほとんどのアプリケーションでは、それは重要ではありません。

おそらくあなたは正しい。しかし、これも特定の言語の目標が何であるかという問題です。実際、多くの言語は、「ほとんど」のニーズに対応するか、他の懸念よりも安全性を優先するように設計されています。CやC ++などのその他のものは、効率を優先します。その文脈では、ほとんどの人が悩まされないという理由だけで全員にパフォーマンスのペナルティを支払わせることは、言語が達成しようとしているものに反します。


-1

そこ良い答えがありますが、私はここで逃した点があると思う:整数オーバーフローの効果は必ずしも悪いことではなく、事後それがいるかどうかを知ることは困難ですiされてから行ってきましたMAX_INTさにMIN_INTオーバーフローの問題が原因でしたまたは、-1を掛けることによって意図的に行われた場合

たとえば、0より大きいすべての表現可能な整数を一緒にfor(i=0;i>=0;++i){...}追加したい場合は、追加ループを使用します。オーバーフローすると、目標の動作です(エラーをスローすると、回避する必要があります)標準の算術を妨げるため、任意の保護)。次の理由により、プリミティブな算術演算を制限するのは悪い習慣です。

  • それらはすべてで使用されています-プリミティブ数学のスローダウンは、すべての機能しているプログラムのスローダウンです
  • プログラマーが必要な場合は、いつでも追加できます
  • それらがあり、プログラマがそれらを必要としない場合(しかし、より速いランタイムが必要な場合)、彼らは最適化のためにそれらを簡単に削除することはできません
  • あなたがそれらを持っていて、プログラマーがそこにいないことを必要とする場合(上記の例のように)、プログラマーはランタイムヒット(関連性があるかもしれないし、そうでないかもしれない)を取り、プログラマーはまだ削除に時間をかける必要があるまたは「保護」を回避します。

3
プログラマーが、言語がそれを提供しない場合、効率的なオーバーフローチェックを追加することは実際に不可能です。関数が無視される値を計算する場合、コンパイラは計算を最適化できます。関数は、オーバーフロー確認ですが、値を計算した場合それ以外の場合は無視し、それがオーバーフローした場合、コンパイラはオーバーフローがそうでなければ、プログラムの出力に影響を与えない、無視することができたとしても、計算やトラップを実行する必要があります。
supercat

1
-1を掛けてからINT_MAXに進むことはできませんINT_MIN
デビッドコンラッド

解決策は、明らかに、プログラマーがコードの特定のブロックまたはコンパイル単位でチェックをオフにする方法を提供することです。
デビッドコンラッド

for(i=0;i>=0;++i){...}私のチームで落胆させようとしているコードのスタイルです。それは特殊効果/副作用に依存しており、それが何を意味するのかを明確に表現していません。しかし、それでも別のプログラミングパラダイムを示しているので、あなたの答えに感謝します。
ベルンハルトヒラー

1
@Delioth:iが1秒間に10億回の反復を実行する一貫したサイレントラップアラウンド2の補数動作を備えた実装であっても、そのようなループはint、実行が許可されている場合にのみ最大値を見つけることが保証されます数百年。一貫したサイレントラップアラウンド動作を約束しないシステムでは、そのような動作は、コードがどれだけ長く与えられても保証されません。
-supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.