今日、私は私の友人と話し合い、「コンパイラの最適化」について数時間議論しました。
ときどき、コンパイラの最適化によってバグが発生するか、少なくとも望ましくない動作が発生する可能性があるという点を私は擁護しました。
私の友人は、「コンパイラーは賢い人たちによって作られ、賢いことをする」とまったく反対し、neverは失敗する可能性があります。
彼は私をまったく説得しませんでしたが、私は私のポイントを強化するために実際の例がないことを認めなければなりません。
ここは誰ですか?もしそうなら、コンパイラの最適化が結果のソフトウェアにバグを生み出した実際の例はありますか?誤解している場合は、プログラミングを中止し、代わりに釣りを学ぶべきですか?
コンパイラの最適化により、バグや望ましくない動作が発生する可能性があります。そのため、これらをオフにできます。
1つの例:コンパイラは、メモリの場所への読み取り/書き込みアクセスを最適化して、重複した読み取りや重複した書き込みを排除したり、特定の操作を並べ替えたりすることができます。問題のメモリの場所が単一のスレッドでのみ使用され、実際にメモリである場合、問題はありません。ただし、メモリの場所がハードウェアデバイスである場合IO registerの場合、書き込みの並べ替えまたは削除は完全に間違っている可能性があります。この状況では、通常、コンパイラが「最適化」する可能性があることを知っているコードを記述する必要がありますそれ、したがって、素朴なアプローチが機能しないことを知っています。
更新:Adam Robinsonがコメントで指摘したように、上記で説明したシナリオは、オプティマイザエラーというよりもプログラミングエラーです。しかし、私が説明しようとしていた点は、他の点では正しく、いくつかの最適化と組み合わせると、他の点では適切に機能するプログラムを組み合わせると、プログラムにバグが発生する可能性があるということです。場合によっては、言語仕様に「この種の最適化が行われ、プログラムが失敗する可能性があるため、この方法で行う必要があります」とありますが、その場合はコードのバグです。ただし、コンパイラーが(通常はオプションの)最適化機能を備えている場合があります。これは、コンパイラーがコードを最適化しようとしすぎているか、最適化が不適切であることを検出できないためです。この場合、プログラマは問題の最適化をオンにしても安全かどうかを知る必要があります。
別の例: linuxカーネルにはバグがありました NULLである可能性があるポインタのテストの前に、NULLである可能性のあるポインタが逆参照されていました。ただし、場合によっては、メモリをアドレス0にマップして、逆参照を成功させることができます。コンパイラーは、ポインターが逆参照されたことに気づいたとき、ポインターをNULLにすることはできないと想定し、後でNULLテストとそのブランチ内のすべてのコードを削除しました。 これにより、コードにセキュリティの脆弱性が導入されました。これは、関数が攻撃者が提供したデータを含む無効なポインタを使用し続けるためです。ポインタが正当にnullであり、メモリがアドレス0にマップされていなかった場合でも、カーネルは以前と同じようにOOPSのままでした。したがって、最適化前のコードには1つのバグが含まれていました。それは2つを含み、そのうちの1つはローカルルートのエクスプロイトを許可した後。
CERTにはプレゼンテーションがあります Robert C. Seacordによって「危険な最適化と因果関係の喪失」と呼ばれ、プログラムにバグを導入(または公開)する多くの最適化をリストしています。 「ハードウェアの機能の実行」から「未定義の可能性のあるすべての動作のトラップ」、「許可されていないものの実行」まで、可能なさまざまな種類の最適化について説明します。
積極的に最適化するコンパイラーが手に入るまで完全に問題のないコードの例:
オーバーフローをチェックしています
// 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言語の仕様は開発者とコンパイラーの間の契約ですが、合意の条件は時間の経過とともに変更される可能性があり、誰もが詳細をすべて理解しているわけではありません。
これが、ほとんどのコンパイラが最適化をオフ(またはオン)にするフラグを提供する理由です。プログラムは整数がオーバーフローする可能性があることを理解して書かれていますか?次に、オーバーフローの最適化をオフにする必要があります。バグが発生する可能性があるためです。プログラムはエイリアスポインタを厳密に回避していますか?次に、ポインタがエイリアスされないことを前提とする最適化をオンにすることができます。プログラムは、情報の漏えいを防ぐためにメモリをクリアしようとしますか?ああ、その場合、あなたは運が悪いです:デッドコードの削除をオフにする必要があるか、コンパイラーが「デッド」コードを排除し、いくつかの作業を使用することを事前に知っておく必要があります-そのため。
最適化を無効にすることでバグが解消されても、ほとんどの場合それはまだあなたの責任です
私は主にC++で記述された商用アプリを担当しています-VC5から始まり、早期にVC6に移植され、現在はVC2008に正常に移植されています。過去10年間で100万回線を超えました。
その間、積極的な最適化を有効にすると、単一のコード生成バグが発生したことを確認できました。
それで、なぜ私は不平を言うのですか?同時に、何十ものバグがコンパイラを疑わせたので、C++標準に対する私の理解が不十分であることが判明しました。この標準は、コンパイラーが利用する場合と利用しない場合がある最適化の余地を作ります。
何年にもわたってさまざまなフォーラムで、コンパイラを非難し、最終的には元のコードのバグであることが判明した多くの投稿を見てきました。間違いなく、それらの多くは、標準で使用されている概念の詳細な理解を必要とするバグを覆い隠していますが、それでもソースコードのバグです。
なぜ私がそんなに遅く返信するのか:実際にコンパイラーの責任であると確認する前にコンパイラーを非難しないでください。
コンパイラー(およびランタイム)の最適化は確かに不要動作を導入する可能性がありますが、少なくともすべきが不特定の動作に依存している場合(または、実際に誤った想定を行っている場合)にのみ発生します。指定された動作)。
さて、それを超えて、もちろんコンパイラはそれらにバグを持つことができます。それらのいくつかは最適化に関連している可能性があり、その影響は非常に微妙である可能性があります-実際、明らかなバグが修正される可能性が高いため、実際には可能性が高いです。
コンパイラーとしてJITを含めるとすると、.NET JITとHotspot JVM(現時点では残念ながら詳細はありません)のリリースされたバージョンにバグがあり、特に奇妙な状況で再現可能でした。それらが特定の最適化によるものかどうかはわかりません。
他の投稿を組み合わせるには:
コンパイラは、ほとんどのソフトウェアと同様に、コードにバグがある場合があります。 NASAの衛星やスマートピープルによって構築された他のアプリにもバグがあるため、「スマートピープル」の議論はこれとはまったく無関係です。最適化を行うコーディングは、行わないコーディングとは異なります。そのため、バグがオプティマイザーにある場合、最適化されていないコードにはエラーが含まれず、最適化されたコードにエラーが含まれる可能性があります。
Shiny氏とNew氏が指摘したように、同時実行性やタイミングの問題に関してナイーブなコードは、最適化なしで問題なく実行でき、最適化で失敗する可能性があります。これにより、実行のタイミングが変わる可能性があります。このような問題はソースコードのせいにすることもできますが、最適化したときにのみ問題が発生する場合は、最適化のせいにする人もいます。
ほんの一例:数日前、誰か discovered オプション-foptimize-sibling-calls
(-O2
によって暗示される)を持つそのgcc 4.5は、起動時にsegfaultするEmacs実行可能ファイルを生成します。
これは どうやら修正された 以降です。
ディレクティブでプログラムの動作を変更できないコンパイラについて聞いたことも、使用したこともありません。通常これは良いことですが、マニュアルを読む必要があります。
そして、コンパイラディレクティブがバグを「削除」した最近の状況がありました。もちろん、バグはまだ残っていますが、プログラムを適切に修正するまで、一時的な回避策があります。
はい。良い例は、ダブルチェックされたロックパターンです。 C++では、ダブルチェックロックを安全に実装する方法はありません。コンパイラーは、マルチスレッドシステムではなくシングルスレッドシステムでは意味のある方法で命令を並べ替えることができるためです。完全な議論は http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf で見つけることができます
ありそうですか?主要製品ではありませんが、確かに可能です。コンパイラの最適化は生成されたコードです。コードがどこから来たとしても(それを書いたり、何かを生成したり)、エラーが含まれる可能性があります。
古いコードをビルドする新しいコンパイラーでこれに数回遭遇しました。古いコードは機能しますが、不適切に定義された/キャスト演算子のオーバーロードなど、場合によっては未定義の動作に依存していました。 VS2003またはVS2005デバッグビルドで機能しますが、リリースではクラッシュします。
生成されたアセンブリを開くと、コンパイラが問題の関数の機能の80%を削除したことは明らかでした。未定義の動作を使用しないようにコードを書き直したところ、コードはクリアされました。
より明白な例:VS2008とGCC
宣言:
_Function foo( const type & tp );
_
呼ばれる:
_foo( foo2() );
_
ここで、foo2()
は、クラスtype
のオブジェクトを返します。
この場合、オブジェクトがスタックに割り当てられていないため、GCCでクラッシュする傾向がありますが、VSはこれを回避するためにいくつかの最適化を行い、おそらく動作します。
エイリアシングは特定の最適化で問題を引き起こす可能性があります。そのため、コンパイラーはそれらの最適化を無効にするオプションを備えています。 Wikipedia から:
予測可能な方法でこのような最適化を可能にするために、Cプログラミング言語(新しいC99エディションを含む)のISO標準では、異なるタイプのポインターが同じメモリー位置を参照することは(一部の例外はあるが)違法であると規定しています。このルールは「厳密なエイリアシング」と呼ばれ、パフォーマンスを大幅に向上させます[引用が必要]が、他の有効なコードを壊すことが知られています。いくつかのソフトウェアプロジェクトは、意図的にC99標準のこの部分に違反しています。たとえば、Python 2.xは参照カウントを実装するためにそうしました。[1]とPython 3の基本オブジェクト構造に必要な変更を加えて、この最適化を有効にしました。厳密なエイリアスがインラインコードの最適化で問題を引き起こすため、Linuxカーネルがこれを行います。 。
はい、コンパイラの最適化は危険な場合があります。通常、ハードリアルタイムソフトウェアプロジェクトは、まさにこの理由で最適化を禁止します。とにかく、バグのないソフトウェアを知っていますか?
積極的な最適化では、変数がキャッシュされたり、奇妙な仮定が行われたりすることがあります。問題は、コードの安定性だけでなく、デバッガーをだますこともできます。いくつかの最適化はマイクロのレジスター内で変数値を保持したため、デバッガーがメモリの内容を表すことができないのを何度か見ました
まったく同じことがあなたのコードにも起こり得ます。最適化は変数をレジスターに入れ、終了するまで変数に書き込みません。次に、コードにスタック内の変数へのポインターがあり、いくつかのスレッドがある場合に、どのように異なるかを想像してください。
理論的には可能です。しかし、あなたが彼らがすることになっていることをするためのツールを信頼しないなら、なぜそれらを使うのですか?しかし、すぐに、
「コンパイラーは賢い人によって作られ、賢いことをする」ので、決して失敗することはありません。
愚かな議論をしている。
それで、コンパイラがそうしていると信じる理由があるまで、なぜそれについての姿勢なのでしょうか?
有りうる。 Linux にも影響を与えています。
コンパイラーは「賢い人」によって書かれているため、間違いがないと言っても馬鹿げていると私は確かに同意します。賢い人々もヒンデンベルクとタコマナロウズブリッジを設計しました。コンパイラライターが最も賢いプログラマの1つであることは事実であっても、コンパイラが最も複雑なプログラムの1つであることも事実です。もちろんバグがあります。
一方、商用コンパイラの信頼性は非常に高いことが経験からわかります。私は何度も何度も誰かが私にプログラムが機能しない理由はコンパイラのバグが原因であるに違いないと私に言ったことがありました、彼はそれを非常に注意深くチェックし、それが100%正しいことを確信しているためです...そして、実際にはプログラムではなくコンパイラにエラーがあることがわかりました。私が個人的に、コンパイラーのエラーであると本当に確信している何かに遭遇したときを思い出そうとしています。思い出せるのは1つの例だけです。
したがって、一般的には、コンパイラを信頼します。しかし、彼らは間違っているのでしょうか?承知しました。
私が覚えているように、初期のDelphi 1には、MinとMaxの結果が逆になるバグがありました。また、浮動小数点値がDLL内で使用された場合にのみ、一部の浮動小数点値に不明瞭なバグがありました。確かに、それは10年以上になったので、私の記憶は少しあいまいかもしれません。
コンパイラーの最適化により、コードの休止(または非表示)バグを明らかに(またはアクティブ化)できます。 C++コードに知らないバグがあるかもしれませんが、それは表示されません。その場合、コードのその分岐が[十分な回数]実行されないため、それは隠れたまたは休止中のバグです。
コードのバグの可能性は、コンパイラーのコードのバグよりもはるかに大きい(数千倍)。コンパイラーは広範囲にわたってテストされているためです。 TDDに加えて、リリース以降にTDDを使用したすべての人が実際に使用します。そのため、バグがあなたによって発見され、文字通り何十万回もそれが他の人々によって使用されて発見されないことはほとんどありません。
休止バグまたは隠されたバグは、まだプログラマーには明らかにされていないバグです。 C++コードに(非表示の)バグがないと主張できる人は非常にまれです。 C++の知識(それを主張できる人はほとんどいません)とコードの広範なテストが必要です。プログラマだけでなく、コード自体(開発スタイル)も重要です。バグが発生しやすいことは、コードの特性(どの程度厳密にテストされているか)またはプログラマー(規律がテストされているか、C++とプログラミングをよく知っているか)にあります。
セキュリティ+同時実行のバグ:同時実行とセキュリティをバグとして含めると、これはさらに悪化します。しかし、結局のところ、これらは「バグ」です。並行性とセキュリティの点でバグのないそもそものコードを書くことはほとんど不可能です。そのため、コードには常にバグが常に存在しており、コンパイラの最適化で明らかになる(または忘れられる)可能性があります。
最適化を使用してビルドする場合、.NET 3.5で問題が発生しました。同じスコープ内の同じ型の既存の変数と同様の名前のメソッドに別の変数を追加すると、2つのうちの1つ(新しい変数または古い変数)は実行時に有効であり、無効な変数へのすべての参照が他への参照に置き換えられます。
たとえば、MyCustomClassタイプのabcdとMyCustomClassタイプのabdcがあり、abcd.a = 5とabdc.a = 7を設定した場合、両方の変数のプロパティはa = 7になります。この問題を修正するには、両方の変数を削除し、プログラムを(できればエラーなしで)コンパイルしてから、再度追加する必要があります。
Silverlightアプリケーションも実行しているときに、.NET 4.0とC#でこの問題に数回遭遇したと思います。私の最後の仕事で、私たちはC++で頻繁に問題に遭遇しました。コンパイルに15分かかり、必要なライブラリのみをビルドするためであった可能性がありますが、新しいコードが追加されていて、ビルドエラーが報告されていなくても、最適化されたコードが以前のビルドとまったく同じである場合がありました。
はい、コードオプティマイザは賢い人々によって構築されています。また、非常に複雑であるため、バグが発生することもよくあります。大規模な製品の最適化されたリリースを完全にテストすることをお勧めします。通常、使用が制限されている製品は完全なリリースに値しませんが、一般的なテストを行って、一般的なタスクが正しく実行されることを確認する必要があります。
プログラムを使用して、またはプログラムに対して実行することを想像できるすべてのことは、バグをもたらします。
exhaustiveテストと実際のC++コードの比較的単純さ(C++には100個未満のキーワード/演算子がある)のため、コンパイラのバグは比較的まれです。悪いプログラミングスタイルはしばしばそれらに遭遇する唯一のものです。そして通常、コンパイラはクラッシュするか、内部コンパイラエラーを生成します。このルールの唯一の例外はGCCです。 GCC、特に古いバージョンでは、O3
で多くの実験的な最適化が有効になっており、他のOレベルも有効になっている場合があります。また、GCCは非常に多くのバックエンドを対象としているため、中間表現のバグの余地が多く残されています。
私は大規模なエンジニアリングアプリケーションに取り組んでいますが、クライアントから報告されたリリースのみのクラッシュやその他の問題が時々見られます。コードには37個のファイル(約6000個中)があり、ファイルの先頭にこれを配置して、このようなクラッシュを修正するための最適化をオフにします。
#pragma optimize( "", off)
(Microsoft Visual C++ネイティブ2015を使用していますが、最適化をまだ行っていないIntel Fortran 2016 update 2を除いて、ほぼすべてのコンパイラーに当てはまります。)
Microsoft Visual Studioフィードバックサイトを検索すると、最適化のバグもいくつか見つかります。時々、一部のログを記録し(コードの小さなセクションで十分に簡単に再現でき、時間をかけても構わない場合)、それらは修正されますが、悲しいことに他のものが再び紹介されます。 笑顔
コンパイラーは人々によって書かれたプログラムであり、どんな大きなプログラムにもバグがあります。コンパイラの最適化オプションには間違いなくバグがあり、最適化をオンにすると、プログラムにバグが発生する可能性があります。
コンパイルするプログラムに優れたテストスイートがある場合は、より積極的な最適化を有効にできます。次に、そのスイートを実行して、プログラムが正しく動作することをいくらか確認することができます。また、本番環境で実行する予定のテストと密接に一致する独自のテストを準備することもできます。
また、大規模なプログラムには、コンパイルに使用するスイッチに個別にいくつかのバグがある可能性があります(おそらく実際にバグがあります)。