ほとんどの C または C++ 環境では、「デバッグ」モードと「リリース」モードのコンパイルがあります。
2 つの違いを見ると、デバッグ モードではデバッグ シンボル (多くの場合、多くのコンパイラで -g オプション) が追加されますが、ほとんどの最適化も無効になります。
「リリース」モードでは、通常、あらゆる種類の最適化がオンになっています。
違いはなぜですか?
6 に答える
最適化をオンにしないと、コードのフローは直線的になります。5 行目でシングル ステップの場合は、6 行目に進みます。最適化をオンにすると、命令の並べ替え、ループの展開、およびあらゆる種類の最適化を行うことができます。
例えば:
void foo() {
1: int i;
2: for(i = 0; i < 2; )
3: i++;
4: return;
この例では、最適化を行わないと、コードを 1 ステップ実行して、1、2、3、2、3、2、4 行目にヒットする可能性があります。
最適化をオンにすると、次のような実行パスが得られる場合があります: 2、3、3、4、または単に 4! (関数は結局何もしません...)
結論として、最適化を有効にしてコードをデバッグすることは、王様の苦痛になる可能性があります。特に大規模な関数がある場合。
最適化をオンにするとコードが変更されることに注意してください。特定の環境 (安全性が重要なシステム) では、これは受け入れられず、デバッグされるコードは出荷されたコードでなければなりません。その場合、最適化をオンにしてデバッグする必要があります。
最適化されたコードと最適化されていないコードは「機能的に」同等である必要がありますが、特定の状況下では動作が変わります。
簡単な例を次に示します。
int* ptr = 0xdeadbeef; // some address to memory-mapped I/O device
*ptr = 0; // setup hardware device
while(*ptr == 1) { // loop until hardware device is done
// do something
}
最適化をオフにすると、これは簡単で、何を期待するかがわかります。ただし、最適化をオンにすると、いくつかのことが発生する可能性があります。
- コンパイラは while ブロックを最適化する可能性があります (0 に初期化しますが、1 になることはありません)。
- メモリにアクセスする代わりに、ポインタ アクセスがレジスタに移動される可能性があります -> No I/O Update
- メモリ アクセスがキャッシュされる可能性があります (必ずしもコンパイラの最適化に関連するとは限りません)。
これらすべてのケースで、動作は大幅に異なり、おそらく間違っています。
デバッグとリリースのもう1つの重要な違いは、ローカル変数の格納方法です。概念的には、ローカル変数は関数スタックフレームでストレージに割り当てられます。コンパイラによって生成されたシンボルファイルは、スタックフレーム内の変数のオフセットをデバッガに通知するため、デバッガはそれを表示できます。デバッガーはこれを行うためにメモリ位置を覗き見します。
ただし、これは、ローカル変数が変更されるたびに、そのソース行に対して生成されたコードがスタック上の正しい場所に値を書き戻す必要があることを意味します。これは、メモリのオーバーヘッドのために非常に非効率的です。
リリースビルドでは、コンパイラは関数の一部のレジスタにローカル変数を割り当てる場合があります。場合によっては、スタックストレージがまったく割り当てられないことがあります(マシンのレジスタが多いほど、これが簡単になります)。
ただし、デバッガーは、レジスターがコード内の特定のポイントのローカル変数にどのようにマップされるかを認識していないため(この情報を含むシンボル形式を認識していません)、正確に表示することはできません。それを探しにどこに行くべきかわからない。
もう1つの最適化は、関数のインライン化です。最適化されたビルドでは、関数が十分に小さいため、コンパイラーはfoo()の呼び出しを、使用されるすべての場所でfooの実際のコードに置き換えることができます。ただし、foo()にブレークポイントを設定しようとすると、デバッガーはfoo()の命令のアドレスを知りたがり、これに対する簡単な答えはなくなります。foo()のコピーが何千もある可能性があります。 )プログラム全体に広がるコードバイト。デバッグビルドは、ブレークポイントを設定する場所があることを保証します。
コードの最適化は、セマンティクスを維持しながらコードの実行時パフォーマンスを向上させる自動化されたプロセスです。このプロセスにより、式または関数の評価を完了するのに不要な中間結果を削除できますが、デバッグ時には必要になる場合があります。同様に、最適化によって見かけ上の制御フローが変更される可能性があるため、ソース コードに表示される順序とは少し異なる順序で処理が行われる可能性があります。これは、不要または冗長な計算をスキップするために行われます。このコードの再調整は、ソース コードの行番号とオブジェクト コードのアドレスの間のマッピングを台無しにする可能性があり、記述したとおりにデバッガーが制御の流れをたどることが難しくなります。
最適化されていないモードでデバッグすると、オプティマイザーが何かを削除したり並べ替えたりしなくても、書いたとおりにすべての内容を確認できます。
プログラムが正しく動作することに満足したら、最適化を有効にしてパフォーマンスを向上させることができます。最近のオプティマイザーはかなり信頼できますが、プログラムが最適化モードと非最適化モードの両方で (パフォーマンスを考慮せずに機能的な観点から) 同じように実行されることを確認するために、高品質のテスト スイートを作成することをお勧めします。
デバッグバージョンが-デバッグされることが期待されます!ブレークポイントの設定、変数の監視中のシングルステップ、スタックトレース、およびデバッガー(IDEまたはその他)で行うその他すべてのことは、空でない、コメントのないソースコードのすべての行がマシンコード命令と一致する場合に意味があります。
ほとんどの最適化は、マシンコードの順序を混乱させます。ループ展開は良い例です。一般的な部分式は、ループから外すことができます。最適化をオンにすると、最も単純なレベルでも、マシンコードレベルでは存在しない行にブレークポイントを設定しようとしている可能性があります。ローカル変数がCPUレジスタに保持されているため、または存在しないように最適化されているために、ローカル変数を監視できない場合があります。
ソースレベルではなく命令レベルでデバッグしている場合、最適化されていない命令をソースにマッピングするのは非常に簡単です。また、コンパイラーはオプティマイザーでバグがある場合があります。
MicrosoftのWindows部門では、すべてのリリースバイナリは、デバッグシンボルと完全な最適化を使用して構築されています。シンボルは個別のPDBファイルに保存され、コードのパフォーマンスには影響しません。これらは製品に同梱されていませんが、ほとんどはMicrosoftSymbolServerで入手できます。
最適化に関するもう 1 つの問題は、インライン関数です。これは、インライン関数を常にシングル ステップで処理するという意味でも同様です。
デバッグと最適化が一緒に有効になっている GCC では、何を期待すべきかわからない場合、コードが間違った動作をしていて、同じステートメントを何度も再実行していると思うでしょう。また、最適化がオンになっている GCC によって提供されるデバッグ情報は、実際よりも品質が低くなる傾向があります。
ただし、Java のような仮想マシンによってホストされる言語では、最適化とデバッグを共存させることができます。デバッグ中であっても、ネイティブ コードへの JIT コンパイルは続行され、デバッグされたメソッドのコードのみが最適化されていないバージョンに透過的に変換されます。
使用されているオプティマイザーにバグがあるか、コード自体にバグがあり、部分的に未定義のセマンティクスに依存していない限り、最適化によってコードの動作が変更されるべきではないことを強調したいと思います。後者は、マルチスレッド プログラミングやインライン アセンブリも使用する場合によく使用されます。
デバッグ シンボルを含むコードはサイズが大きくなり、キャッシュ ミスが増える可能性があります。つまり、速度が低下し、サーバー ソフトウェアの問題になる可能性があります。
少なくとも Linux では (Windows が異なる理由はありません)、デバッグ情報はバイナリの別のセクションにパッケージ化されており、通常の実行時には読み込まれません。これらは、デバッグに使用する別のファイルに分割できます。また、一部のコンパイラ (Gcc を含む、Microsoft の C コンパイラも同様だと思います) では、デバッグ情報と最適化の両方を一緒に有効にすることができます。そうでない場合、明らかにコードが遅くなります。