74

今日、友人と「コンパイラの最適化」について数時間議論しました。

私は、コンパイラの最適化によってバグが発生したり、少なくとも望ましくない動作が発生したりする可能性があるという点を擁護しました。

私の友人は、「コンパイラは賢い人々によって構築され、賢いことを行う」と言って、完全に反対しました。

彼は私をまったく納得させませんでしたが、私の主張を補強する実例が不足していることを認めなければなりません。

ここにいるのは誰?私がそうである場合、コンパイラの最適化によって結果として得られるソフトウェアにバグが発生した実際の例はありますか? もし私が間違っていたら、プログラミングをやめて代わりに釣りを学ぶべきですか?

4

22 に答える 22

45

コンパイラの最適化により、バグや望ましくない動作が発生する可能性があります。そのため、それらをオフにすることができます。

1 つの例: コンパイラーは、メモリー位置への読み取り/書き込みアクセスを最適化し、重複読み取りまたは重複書き込みを排除したり、特定の操作の順序を変更したりすることができます。問題のメモリ ロケーションが単一のスレッドによってのみ使用され、実際にメモリである場合は、問題ない可能性があります。しかし、メモリの場所がハードウェア デバイスの IO レジスタである場合、書き込みの並べ替えや削除は完全に間違っている可能性があります。この状況では、通常、コンパイラーがコードを「最適化」する可能性があることを認識してコードを作成する必要があるため、単純なアプローチが機能しないことを認識しています。

アップデート:Adam Robinson がコメントで指摘したように、私が上で説明したシナリオは、オプティマイザーのエラーというよりはプログラミングのエラーです。しかし、私が説明しようとしていたポイントは、他の点では正しく動作するいくつかの最適化と組み合わされたいくつかのプログラムは、それらが組み合わされたときにプログラムにバグを導入する可能性があるということです. 場合によっては、言語仕様に「この種の最適化が行われる可能性があり、プログラムが失敗する可能性があるため、この方法で行う必要があります」と記載されている場合があります。この場合、それはコードのバグです。ただし、コンパイラが (通常はオプションの) 最適化機能を持っている場合があります。これは、コンパイラがコードを最適化しようと試みすぎたり、最適化が不適切であることを検出できないために、正しくないコードを生成する可能性があるためです。

別の例: Linux カーネルには、 NULL である可能性のあるポインターが null であるかどうかのテストの前に逆参照されるというバグがありました。ただし、場合によっては、メモリをアドレス 0 にマップして、逆参照を成功させることができました。コンパイラは、ポインターが逆参照されたことに気付くと、それが NULL であってはならないと想定し、後で NULL テストとその分岐内のすべてのコードを削除しました。これにより、コードにセキュリティの脆弱性が導入されましたこれは、攻撃者が提供したデータを含む無効なポインターを関数が使用するためです。ポインターが合法的に null であり、メモリがアドレス 0 にマップされていない場合、カーネルは以前と同様に OOPS を返します。そのため、最適化前のコードには 1 つのバグが含まれていました。2 つ含まれていた後、そのうちの 1 つがローカルの root エクスプロイトを可能にしました。

CERT には、 Robert C. Seacord による「Dangerous Optimizations and the Loss of Causality」というプレゼンテーションがあり、プログラムにバグを導入 (または公開) する多くの最適化がリストされています。「ハードウェアが行うことを行う」から「考えられるすべての未定義の動作をトラップする」、「許可されていないことを行う」まで、可能なさまざまな種類の最適化について説明します。

積極的に最適化するコンパイラが手に入れるまでは、まったく問題のないコードの例をいくつか示します。

  • オーバーフローのチェック

    // fails because the overflow test gets removed
    if (ptr + len < ptr || ptr + len > max) return EINVAL;
    
  • オーバーフロー算術をまったく使用する:

    // The compiler optimizes this to an infinite loop
    for (i = 1; i > 0; i += i) ++j;
    
  • 機密情報のメモリをクリアする:

    // the compiler can remove these "useless writes"
    memset(password_buffer, 0, sizeof(password_buffer));
    

ここでの問題は、何十年もの間、コンパイラが最適化にあまり積極的でなかったことです。そのため、C プログラマーの世代は、固定サイズの 2 の補数の加算やそれがどのようにオーバーフローするかなどを学び、理解しています。その後、コンパイラ開発者によって C 言語の標準が修正され、ハードウェアが変わらないにもかかわらず、微妙なルールが変更されます。C 言語仕様は開発者とコンパイラの間の契約ですが、契約の条件は時間の経過とともに変更される可能性があり、すべての人がすべての詳細を理解しているわけではなく、詳細が理にかなっているとさえ同意しているわけではありません。

これが、ほとんどのコンパイラが最適化をオフ (またはオン) にするフラグを提供する理由です。あなたのプログラムは、整数がオーバーフローする可能性があることを理解して書かれていますか? 次に、バグが発生する可能性があるため、オーバーフローの最適化をオフにする必要があります。あなたのプログラムは、ポインターのエイリアシングを厳密に回避していますか? 次に、ポインターがエイリアス化されないことを前提とした最適化をオンにできます。あなたのプログラムは、情報の漏洩を避けるためにメモリをクリアしようとしますか? ああ、その場合は運が悪い: デッド コード リムーバルをオフにするか、コンパイラが「デッド」コードを削除して何らかの作業を行うことを前もって知る必要がある-それについて。

于 2010-04-27T15:05:44.623 に答える
34

最適化を無効にすることでバグが解消されても、ほとんどの場合、それはまだあなたのせいです

私は、主にC ++で記述された商用アプリを担当しています。VC5から始まり、早期にVC6に移植され、現在はVC2008に正常に移植されています。過去10年間で100万ラインを超えました。

その時、積極的な最適化が有効になっているときに、単一のコード生成バグが発生したことを確認できました。

では、なぜ私は不平を言っているのですか?同時に、コンパイラを疑わせるバグが数十個あったためですが、C++標準についての理解が不十分であることが判明しました。この標準は、コンパイラーが使用する場合と使用しない場合がある最適化の余地を作ります。

何年にもわたってさまざまなフォーラムで、コンパイラを非難する多くの投稿を目にしてきましたが、最終的には元のコードのバグであることが判明しました。それらの多くは、標準で使用されている概念を詳細に理解する必要があるバグを覆い隠していることは間違いありませんが、それでもソースコードのバグです。

返信が遅い理由:実際にコンパイラのせいであると確認する前に、コンパイラのせいにするのはやめましょう。

于 2010-04-28T06:19:33.887 に答える
15

コンパイラ (およびランタイム) の最適化は、確かに望ましくない動作を引き起こす可能性がありますが、少なくとも、未指定の動作に依存している (または、明確に指定された動作について誤った仮定をしている) 場合にのみ発生するはずです。

それを超えて、もちろんコンパイラにはバグが含まれている可能性があります。それらのいくつかは最適化に関連している可能性があり、その影響は非常に微妙である可能性があります-明らかなバグが修正される可能性が高いため、実際にそうなる可能性があります.

コンパイラとして JIT が含まれていると仮定すると、リリースされたバージョンの .NET JIT と Hotspot JVM の両方にバグが見られました (残念ながら現時点では詳細はわかりません)。それらが特定の最適化によるものかどうかはわかりません。

于 2010-04-27T15:08:07.647 に答える
11

他の投稿を結合するには:

  1. ほとんどのソフトウェアと同様に、コンパイラにはコードにバグが含まれていることがあります。NASA の人工衛星やその他のスマートな人々によって構築されたアプリにもバグがあるため、「スマートな人々」の議論はこれとはまったく関係ありません。最適化を行うコーディングは、最適化を行わないコーディングとは異なるため、オプティマイザーにバグが発生した場合、実際には、最適化されていないコードには含まれないものの、最適化されたコードにはエラーが含まれる可能性があります。

  2. Shiny 氏と New 氏が指摘したように、同時実行性やタイミングの問題に関してナイーブなコードが、最適化なしで十分に実行されても、実行のタイミングが変わる可能性があるため、最適化で失敗する可能性があります。このような問題をソース コードのせいにすることもできますが、最適化された場合にのみ現れる場合は、最適化のせいにする人もいます。

于 2010-04-27T15:12:19.233 に答える
8

ほんの一例: 数日前、gcc 4.5 にオプション( によって暗示されている) を指定すると、起動時にセグメンテーション違反を起こす Emacs 実行可能ファイルが生成されることが誰かによって発見されました。-foptimize-sibling-calls-O2

これは明らかに修正されています。

于 2010-04-27T15:07:18.507 に答える
8

ディレクティブがプログラムの動作を変更できないコンパイラについて聞いたことも、使用したこともありません。一般的にこれは良いことですが、マニュアルを読む必要があります。

そして、最近、コンパイラ ディレクティブによってバグが「削除」されたという状況がありました。もちろん、バグは実際にはまだ残っていますが、プログラムを適切に修正するまでの一時的な回避策があります。

于 2010-04-27T15:09:10.003 に答える
7

はい。良い例は、ダブルチェック ロック パターンです。C++ では、ダブルチェック ロックを安全に実装する方法はありません。コンパイラは、シングル スレッド システムでは意味のある方法で命令を並べ替えることができますが、マルチスレッド システムでは意味がありません。完全な議論はhttp://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdfで見つけることができます。

于 2010-04-27T15:11:52.033 に答える
6

ありそうですか?主要な製品ではありませんが、確かに可能です。コンパイラの最適化は生成されたコードです。コードがどこから来たものであろうと (あなたが書いたものであれ、何かがそれを生成したものであれ)、エラーが含まれている可能性があります。

于 2010-04-27T15:06:52.693 に答える
6

古いコードを構築する新しいコンパイラでこれに数回遭遇しました。古いコードは機能しますが、不適切に定義された / 演算子のオーバーロードをキャストするなど、場合によっては未定義の動作に依存していました。VS2003 または VS2005 のデバッグ ビルドでは機能しますが、リリースではクラッシュします。

生成されたアセンブリを開くと、問題の関数の機能の 80% がコンパイラによって削除されたことが明らかになりました。未定義の動作を使用しないようにコードを書き直すと、問題が解決しました。

より明白な例: VS2008 と GCC の比較

宣言:

Function foo( const type & tp ); 

呼ばれる:

foo( foo2() );

wherefoo2()は class のオブジェクトを返しますtype

この場合、オブジェクトがスタックに割り当てられていないため、GCC でクラッシュする傾向がありますが、VS はこれを回避するためにいくつかの最適化を行い、おそらく動作します。

于 2010-04-27T15:13:00.760 に答える
5

エイリアシングは特定の最適化で問題を引き起こす可能性があるため、コンパイラにはこれらの最適化を無効にするオプションがあります。ウィキペディアから:

このような最適化を予測可能な方法で有効にするために、C プログラミング言語 (新しい C99 版を含む) の ISO 標準では、異なる型のポインターが同じメモリ位置を参照することは (いくつかの例外を除いて) 違法であると規定しています。「厳密なエイリアシング」として知られるこのルールにより、パフォーマンスが大幅に向上しますが[要出典]、有効なコードを壊すことが知られています。いくつかのソフトウェア プロジェクトは、C99 標準のこの部分に意図的に違反しています。たとえば、Python 2.x は参照カウントを実装するためにそうしました[1]。この最適化を可能にするために、Python 3 の基本的なオブジェクト構造体に変更を加える必要がありました。Linux カーネルがこれを行うのは、厳密なエイリアシングがインライン コードの最適化で問題を引き起こすためです[2]。そのような場合、gcc でコンパイルすると、

于 2010-04-27T17:14:24.403 に答える
4

はい、コンパイラの最適化は危険な場合があります。通常、ハード リアルタイム ソフトウェア プロジェクトでは、まさにこの理由で最適化が禁止されます。とにかく、バグのないソフトウェアを知っていますか?

積極的な最適化は、変数をキャッシュしたり、奇妙な仮定を行ったりする可能性があります。問題はコードの安定性にあるだけでなく、デバッガーを騙す可能性もあります。一部の最適化がマイクロのレジスタ内に変数値を保持しているため、デバッガがメモリの内容を表現できないことを何度か見てきました。

まったく同じことがコードに発生する可能性があります。最適化は変数をレジスターに入れ、完了するまで変数に書き込みません。コードにスタック内の変数へのポインターがあり、複数のスレッドがある場合、どのように異なるか想像してみてください

于 2010-04-27T15:14:38.387 に答える
3

理論的には可能です、確かに。しかし、ツールが本来の役割を果たしてくれると信じていないのなら、なぜそれらを使う必要があるのでしょうか? しかしすぐに、

「コンパイラは賢明な人々によって構築され、賢明なことを行う」ため、決して失敗することはありません。

馬鹿げた主張をしている。

では、コンパイラがそうしていると信じるに足る理由が得られるまで、なぜそれについて姿勢を示すのでしょうか?

于 2010-04-27T15:03:59.933 に答える
3

起こり得る。Linuxにも影響を与えています。

于 2010-04-27T15:13:13.913 に答える
3

コンパイラは「賢い人々」によって書かれているので、絶対確実だと言うのはばかげていることに私は確かに同意します。賢い人々は、ヒンデンベルク橋とタコマ橋も設計しました。コンパイラの作成者が世界で最も賢いプログラマであるというのは事実だとしても、コンパイラは世界で最も複雑なプログラムの 1 つであるということも事実です。もちろん、彼らにはバグがあります。

一方、商用コンパイラの信頼性は非常に高いことが経験からわかります。私は何度も、プログラムが動作しない理由は、コンパイラーのバグが原因であるに違いないと誰かが私に言ったことが何度もありました。そして、実際にはコンパイラではなくプログラムにエラーがあることがわかります。私は、コンパイラのエラーであると本当に確信している何かに個人的に出くわしたときのことを考えようとしています.1つの例しか思い出すことができません.

したがって、一般的には、コンパイラを信頼してください。しかし、彼らは間違っていますか?もちろん。

于 2010-04-27T17:02:43.590 に答える
2

最適化を使用してビルドし、同じスコープ内の同じ型の既存の変数と同様の名前のメソッドに別の変数を追加すると、.NET 3.5 で問題が発生しました。2 つのうちの 1 つ (新しい変数または古い変数) は実行時に有効であり、無効な変数へのすべての参照は他の変数への参照に置き換えられます。

たとえば、MyCustomClass タイプの abcd があり、MyCustomClass タイプの abdc があり、abcd.a=5 と abdc.a=7 を設定すると、両方の変数のプロパティは a=7 になります。この問題を解決するには、両方の変数を削除し、プログラムをコンパイルして (できればエラーなしで)、それらを再度追加する必要があります。

Silverlight アプリケーションを実行しているときに、.NET 4.0 と C# でこの問題に何度か遭遇したと思います。私の最後の仕事では、C++ でかなり頻繁に問題に遭遇しました。コンパイルに 15 分かかったため、必要なライブラリのみをビルドしたためかもしれませんが、新しいコードが追加され、ビルド エラーが報告されていないにもかかわらず、最適化されたコードが以前のビルドとまったく同じになる場合がありました。

はい、コード オプティマイザは賢い人々によって構築されています。また、非常に複雑なため、バグが発生することもよくあります。大規模な製品の最適化されたリリースを完全にテストすることをお勧めします。通常、使用が制限された製品は完全なリリースの価値はありませんが、一般的なタスクを正しく実行することを確認するために、一般的にテストする必要があります。

于 2012-04-30T17:40:58.637 に答える
2

思い出したように、初期の Delphi 1 には Min と Max の結果が逆になるというバグがありました。浮動小数点値が dll 内で使用された場合にのみ、一部の浮動小数点値に関するあいまいなバグもありました。確かに10年以上前なので記憶が曖昧かもしれません。

于 2010-04-27T21:04:10.217 に答える
2

コンパイラの最適化は、コード内の休眠 (または隠れた) バグを明らかに (またはアクティブ化) することができます。C++ コードに、あなたが気付いていないだけのバグがあるかもしれません。その場合、コードのその分岐が [十分な回数] 実行されないため、隠れたバグまたは休眠バグです。

コードのバグの可能性は、コンパイラのコードのバグよりもはるかに大きく (数千倍) あります。コンパイラは広範囲にテストされているためです。TDDに加えて、リリース以来それらを使用している実質的にすべての人々によって!)。したがって、バグがあなたによって発見され、文字通り何十万回も使用されている他の人々によって発見されない可能性はほとんどありません。

休眠バグまたは隠れバグは、プログラマーにまだ明らかにされていない単なるバグです。自分の C++ コードに (隠れた) バグがないと主張できる人は非常にまれです。C++ の知識 (それを主張できる人はほとんどいません) とコードの広範なテストが必要です。それはプログラマーだけの問題ではなく、コードそのもの (開発スタイル) の問題です。バグが発生しやすいのは、コードの性質 (どれだけ厳密にテストされているか) またはプログラマー (テストがどれほど厳格で、C++ とプログラミングをどれだけよく知っているか) にあります。

セキュリティ + 同時実行のバグ: 同時実行とセキュリティをバグとして含めると、これはさらに悪化します。しかし、結局のところ、これらは「バグ」です。並行性とセキュリティの観点から、そもそもバグのないコードを書くことはほとんど不可能です。そのため、コンパイラの最適化で明らかになる (または忘れられる) 可能性があるコードには常にバグが存在します。

于 2016-06-03T12:00:33.380 に答える
0

徹底的なテストと実際の C++ コードの比較的単純さ (C++ には 100 未満のキーワード/演算子がある) により、コンパイラのバグは比較的まれです。多くの場合、それらに遭遇するのは悪いプログラミング スタイルだけです。通常、コンパイラはクラッシュするか、代わりに内部コンパイラ エラーを生成します。この規則の唯一の例外は GCC です。GCC、特に古いバージョンでは、多くの実験的な最適化が有効になってO3おり、場合によっては他の O レベルでも有効になっていました。また、GCC は非常に多くのバックエンドを対象としているため、中間表現にバグが発生する余地が多くなります。

于 2012-04-30T17:26:11.047 に答える
0

プログラムで、またはプログラムに対して行うと想像できるすべてのことは、バグを導入します。

于 2010-04-27T17:14:12.453 に答える