コンパイラーがコードを壊したかどうか、そしてそれがコンパイラーだったらどうすればよいかを知るにはどうすればよいですか?


14

あるレベルの最適化でコンパイルすると、C ++コードが機能しない場合があります。それは、コードを壊す最適化を行っているコンパイラかもしれませんし、コンパイラが感じることなら何でもできるようにする未定義の振る舞いを含むコードかもしれません。

より高い最適化レベルでのみコンパイルされたときに壊れるコードの一部があるとします。コードまたはコンパイラーであるかどうか、またコンパイラーである場合はどうすればよいですか?


43
おそらくそのあなた。
-littleadv

9
@ littleadv、gccとmsvcの最近のバージョンでさえバグでいっぱいですので、私はそう確信していません。
SKロジック

3
すべての警告が有効になっていますか?

@ThorbjørnRavn Andersen:はい、有効にしています。
sharptooth

3
1 タイトなCPUループを記述していない限り、多くのアプリケーションでは、PCは基本的にすべての時間をライブラリまたはI / Oに費やしています。そのようなアプリでは、/ Oスイッチはまったく役に立ちません。
マイクダンラベイ

回答:


19

大半の場合、壊れているのはコンパイラではなく、あなたのコードであるというのは安全な賭けだと思います。そして、それがコンパイラーであるという異常なケースでさえ、おそらく特定のコンパイラーが準備されていない、あいまいな方法でいくつかのあいまいな言語機能を使用しているでしょう。つまり、コードをより慣用的なものに変更し、コンパイラの弱点を回避できる可能性が高いと言えます。

とにかく、(言語仕様に基づいて)コンパイラのバグを発見したことを証明できる場合は、コンパイラの開発者に報告してください。


@ SK-logic、結構、私にはそれを裏付ける統計がありません。それは私自身の経験に基づいており、言語やコンパイラの制限をめったに拡張していないことを認めています-他の人がそれをより頻繁に行うかもしれません。
ペテルトレック

(1)@ SK-Logic:C ++コンパイラのバグを発見しました。同じコードが、あるコンパイラで試して動作し、別のコンパイラで試してみましたが、壊れました。
umlcat

8
@umlcat:不特定の動作に依存するコードである可能性が高い。あるコンパイラでは期待に一致しますが、別のコンパイラでは一致しません。それは壊れているという意味ではありません。
ハビエル

@Ritch Melton、LTOを使用したことがありますか?
SKロジック

1
ゲーム機について話すとき、私はCrashworksに同意します。その特定の状況で難解なコンパイラのバグを見つけることはまったく珍しいことではありません。ただし、頻繁に使用されるコンパイラを使用して通常のPCをターゲットにしている場合、これまで誰も見たことのないコンパイラのバグにぶつかることはほとんどありません。
トレバーパウエル

14

いつものように、他のバグと同様に、制御された実験を実行します。疑わしい領域を絞り込み、他のすべての最適化をオフにして、そのコードチャンクに適用される最適化の変更を開始します。100%の再現性が得られたら、コードの変更を開始し、特定の最適化を妨げる可能性のあるものを導入します(たとえば、ポインターエイリアシングの可能性の導入、潜在的な副作用を伴う外部呼び出しの挿入など)。デバッガーでアセンブリコードを確認することも役立ちます。


何に役立つかも?コンパイラのバグである場合-それで何ですか?
-littleadv

2
@littleadv、それがコンパイラのバグである場合、それを修正することを試みることができます(または、完全に詳細に適切に報告するか、これを使用し続ける運命にあるなら、将来それを避ける方法を見つけることができます)しばらくの間、コンパイラのバージョン。多数のC ++のような境界線の問題の1つである独自のコードに問題がある場合、この種の精査は、バグの修正と将来のその種の回避にも役立ちます。
SKロジック

私が答えで言ったように、報告以外に、それが誰の過ちであるかにかかわらず、治療に大きな違いはありません。
-littleadv

3
@littleadv、問題の性質を理解せずに、何度も繰り返し直面する可能性があります。また、コンパイラを自分で修正する可能性があります。そして、はい、残念ながら、C ++コンパイラのバグを見つけることは「ありそうもない」ことではありません。
SKロジック

10

結果のアセンブリコードを調べ、ソースが要求していることを実行するかどうかを確認します。何らかの非自明な方法で実際にあなたのコードが故障している可能性が非常に高いことを覚えておいてください。


1
これは本当にこの質問に対する唯一の答えです。この場合のコンパイラの仕事は、C ++からアセンブリ言語に移行することです。あなたはそれがコンパイラだと思う...コンパイラの動作を確認してください。とても簡単です。
old_timer

7

プログラミングの30年以上で、私が見つけた本物のコンパイラ(コード生成)バグの数はまだ10にすぎません。 > 10,000。私の「経験則」は、特定のバグがコンパイラーに起因する確率は<0.001であるということです。


1
幸運ですね。私の平均は月に約1つの非常に悪いバグであり、マイナーな境界線の問題ははるかに頻繁です。また、使用している最適化レベルが高いほど、コンパイラエラーの可能性が高くなります。-O3とLTOを使用しようとしている場合、すぐにそれらのいくつかを見つけられないことは非常に幸運です。そして、ここではリリースバージョンのバグのみを数えます。コンパイラ開発者として、私の仕事ではこの種の問題にはるかに直面していますが、それは数えません。コンパイラを台無しにするのがどれほど簡単かを知っています。
SKロジック

2
25年、私も非常に多くを見てきました。コンパイラは毎年悪化しています。
old_timer

5

私はコメントを書き始め、それからそのポイントが長すぎて長すぎると判断しました。

私はあなたのコードが壊れていると主張します。万が一、コンパイラのバグを発見した場合は、コンパイラの開発者にバグを報告する必要がありますが、それで違いが終わります。

解決策は、問題のコンストラクトを識別し、リファクタリングして、同じロジックが異なる方法で実行されるようにすることです。バグがあなたの側にあるのかコンパイラにあるのかに関係なく、問題を解決する可能性が最も高いでしょう。


5
  1. コードを完全に読み直してください。ASSERTの副作用や、その他のデバッグ(またはより一般的な構成)固有のステートメントで何かを行っていないことを確認してください。また、デバッグビルドメモリでは異なる初期化が行われることを覚えておいてください-テルテールポインター値は、デバッグ-メモリ割り当て表現で確認できます。Visual Studio内から実行する場合、環境変数でこれが必要でないことを明示的に指定しない限り、ほとんど常に(リリースモードであっても)デバッグヒープを使用します。
  2. ビルドを確認してください。実際のコンパイラ以外の場所で複雑なビルドの問題が発生することはよくあります-依存関係がしばしば犯人です。「完全に再構築してみましたか」は「Windowsを再インストールしてみました」と同じくらい腹立たしい答えですが、多くの場合は役立ちます。試してください:a)再起動します。b)すべての中間ファイルと出力ファイルを手動で削除し、再構築します。
  3. コードを調べて、未定義の動作を呼び出している可能性のある場所を確認してください。しばらくC ++で作業している場合は、「それを仮定することを完全に確信していない...」と思う箇所がいくつかあることがわかります。未定義の動作かどうかを確認するコードのタイプ。
  4. それでも問題が解決しない場合は、問題の原因となっているファイルの前処理済み出力を生成します。予期しないマクロ展開は、あらゆる種類の楽しみを引き起こす可能性があります(同僚がHという名前のマクロを選択するのは良い考えだと思い出しました...)。プロジェクト構成間の予期しない変更について、前処理された出力を調べます。
  5. 最後の手段-今、あなたは本当にコンパイラのバグの世界にいます-アセンブリの出力を見てください。これには、アセンブリが実際に行っていることを把握するために、掘り下げて戦う必要がありますが、実際には非常に有益です。ここで取り上げたスキルを使用してマイクロ最適化を評価することもできるため、すべてが失われることはありません。

「未定義の動作」の場合は+1。私はそれに噛まれました。int + intハードウェアのADD命令にコンパイルされるかのように、オーバーフローに依存するコードを書きました。古いバージョンのGCCでコンパイルした場合は正常に機能しましたが、新しいコンパイラーでコンパイルした場合は正常に機能しませんでした。どうやらGCCの優秀な人々は、整数オーバーフローの結果が未定義であるため、オプティマイザーは決して起こらないという仮定の下で動作する可能性があると判断しました。重要なブランチをコードから最適化しました。
ソロモンスロー

2

コードかコンパイラかを知りたい場合は、C ++の仕様を完全に知る必要があります。

疑わしい場合は、x86アセンブリを完全に理解する必要があります。

両方を完全に学習する気分になっていない場合、コンパイラが最適化レベルに応じて異なる方法で解決するのは、ほぼ確実に未定義の動作です。


(+1)@mouviciel:仕様にある場合でも、機能がコンパイラーによってサポートされているかどうかにも依存します。gccに奇妙なバグがあります。「プレーンポインタ構造」を「関数ポインタ」で宣言します。これは仕様で許可されていますが、状況によっては機能し、別の状況では機能しません。
umlcat

1

オプティマイザが間違っているよりも、標準コードでコンパイルエラーまたは内部コンパイルエラーが発生する可能性が高くなります。しかし、コンパイラがループを最適化することを聞いたことがあります。

そのあなたかコンパイラかを知る方法についての提案はありません。別のコンパイラを試すことができます。

ある日、それが自分のコードかどうか疑問に思っていて、誰かが私にvalgrindを提案しました。私はそれを使ってプログラムを実行するために5分または10分を費やしました(私valgrind --leak-check=yes myprog arg1 arg2はそれをやったと思いますが、他のオプションで遊んだ)とすぐに問題のある特定のケースの下で実行されている1つの行を示しました。その後、奇妙なクラッシュ、エラー、または奇妙な動作が発生することなく、アプリがスムーズに実行されました。valgrindまたはそのような別のツールは、自分のコードかどうかを知る良い方法です。

サイドノート:私はかつて私のアプリのパフォーマンスが下がった理由を疑問に思いました。パフォーマンスの問題もすべて1行に収まっていることがわかりました。私は書いたfor(int i=0; i<strlen(sz); ++i) {。szは数MBでした。何らかの理由で、コンパイラーは最適化後も毎回strlenを実行しました。1つの行が重要な場合があります。パフォーマンスからクラッシュまで


1

ますます一般的な状況としては、コンパイラがCの方言用に書かれたコードを破り、標準で義務付けられていない動作をサポートし、それらの方言をターゲットとするコードが厳密に適合するコードよりも効率的になることを許可します。そのような場合、ターゲットの方言を実装したコンパイラで100%信頼できる「壊れた」コードと記述すること、または必要なセマンティクスをサポートしない方言を処理するコンパイラを「壊れた」と記述することは不公平です。 。代わりに、問題は単に、最適化が有効になっている最新のコンパイラーによって処理された言語が、一般的に使用されていた方言とは異なるという事実に起因しています(そして、最適化が無効になっている多くのコンパイラー、または最適化が有効になっている一部のコンパイラーによってまだ処理されています)。

たとえば、gccの標準の解釈によって義務付けられていない多くのポインターエイリアシングパターンを正当なものとして認識する方言用に多くのコードが記述されており、そのようなパターンを使用してコードのわかりやすい翻訳をより読みやすく効率的にすることができますC標準のgccの解釈の下で可能であるよりも。そのようなコードはgccと互換性がないかもしれませんが、それが壊れていることを意味するものではありません。それは単に、gccが最適化を無効にしてのみサポートする拡張機能に依存しています。


まあ、確かに、Cの標準X +拡張YとZをコーディングすると何も間違っている限りそれはあなたに大きな利点を与え、あなたはそれをした知っている、とあなたはそれを徹底的に文書化。悲しいことに、3つの条件はすべて満たされていないため、コードが壊れていると言っても過言ではありません
デュプリケータ

@Deduplicator:C89コンパイラーは先行モデルと上位互換性があること、およびC99などと同様に上位互換性があると宣伝されました。C89は一部のプラットフォームで以前に定義された動作に要件を課していませんが、上位互換性は定義されたとおりに動作を考慮していたプラットフォームは、引き続きそうする必要があります。短い符号なし型を符号に昇格させる理由は、規格の作成者が、規格が義務付けているかどうかにかかわらず、コンパイラがそのように動作することを期待していることを示唆しています。さらに
...-supercat

...エイリアシングルールの厳密な解釈により、上位互換性が失われ、多くの種類のコードが実行不能になりますが、いくつかの微調整(たとえば、クロスタイプエイリアシングが予想されるため許容されるパターンを特定する)は両方の問題を解決します。ルールの目的は、コンパイラが「悲観的」なエイリアシングを前提とすることを避けることでしたが、「float x」が与えられた場合、「foo」が変更されても「foo((int *)&x)」がxタイプ 'float *'または "char *"のポインターに書き込むことは、「悲観的」または「明白」とみなされませんか?
-supercat

0

問題のある箇所を特定し、観察された動作を言語仕様に従って何が起こるべきかと比較します。間違いなく簡単ではありませんが、それはあなたが知るためにしなければならないことです(そして単に仮定するだけでありません)。

私はおそらくそれほど細心ではありません。むしろ、コンパイラーメーカーのサポートフォーラム/メーリングリストに問い合わせます。それが本当にコンパイラのバグである場合、彼らはそれを修正するかもしれません。とにかく私のコードでしょう。たとえば、スレッド化におけるメモリの可視性に関する言語仕様は直観に反する場合があり、特定のハードウェア(!)で特定の最適化フラグを使用する場合にのみ明らかになる可能性があります。一部の動作は仕様で定義されていない可能性があるため、一部のコンパイラ/一部のフラグでは機能しますが、他の一部では機能しません。


0

ほとんどの場合、コードには未定義の動作があります(他の人が説明したように、C ++コンパイラが非常に複雑でバグがある場合でも、C ++仕様にも設計上のバグがある場合でも、コンパイラよりもコードにバグがある可能性が高くなります) 。また、コンパイルされた実行可能ファイルが(運が悪ければ)動作する場合でも、ここにUBを配置できます。

したがって、Lattnerのブログ「すべてのCプログラマーが未定義の動作について知っておくべきこと」を読む必要があります(そのほとんどはC ++ 11にも当てはまります)。

valgrindののツール、および最近の-fsanitize= 計装オプションGCC(またはクラン/ LLVMは)、また助けになるはずです。そしてもちろん、すべての警告を有効にします:g++ -Wall -Wextra

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