まず第一に、ほとんどの最適化バグは、プログラミング エラーまたは最適化設定 (浮動小数点値、マルチスレッドの問題など) に応じて変化する可能性がある事実に依存していることが原因であることを知っています。
しかし、バグを見つけるのが非常に困難であり、最適化をオフにせずにこの種のエラーが発生するのを防ぐ方法があるかどうか、やや確信が持てません。何か不足していますか?これは本当にオプティマイザのバグでしょうか? 簡単な例を次に示します。
struct Data {
int a;
int b;
double c;
};
struct Test {
void optimizeMe();
Data m_data;
};
void Test::optimizeMe() {
Data * pData; // Note that this pointer is not initialized!
bool first = true;
for (int i = 0; i < 3; ++i) {
if (first) {
first = false;
pData = &m_data;
pData->a = i * 10;
pData->b = i * pData->a;
pData->c = pData->b / 2;
} else {
pData->a = ++i;
} // end if
} // end for
};
int main(int argc, char *argv[]) {
Test test;
test.optimizeMe();
return 0;
}
もちろん、実際のプログラムにはこれ以外にもやるべきことがたくさんあります。しかし、それはすべて、m_data に直接アクセスする代わりに、(以前は初期化された) ポインターが使用されているという事実に要約されます。-partに十分なステートメントを追加するとすぐにif (first)
、オプティマイザーはコードを次の行に沿って何かに変更するようです。
if (first) {
first = false;
// pData-assignment has been removed!
m_data.a = i * 10;
m_data.b = i * m_data.a;
m_data.c = m_data.b / m_data.a;
} else {
pData->a = ++i; // This will crash - pData is not set yet.
} // end if
ご覧のとおり、不要なポインター逆参照をメンバー構造体への直接書き込みに置き換えます。else
ただし、 -branchではこれを行いません。-代入も削除しpData
ます。ポインターはまだ初期化されているため、プログラムは -branch でクラッシュしelse
ます。
もちろん、ここにはさまざまな改善点があるため、プログラマのせいにすることもできます。
- ポインターを忘れて、オプティマイザーが行うことを行います-
m_data
直接使用します。 - pData を nullptr に初期化します。これにより、オプティマイザー
else
は、ポインターが割り当てられない場合に -branch が失敗することを認識します。少なくとも、私のテスト環境では問題を解決しているようです。 - ループの前にポインターの割り当てを移動します (効果的に で初期化
pData
し&m_data
ます。これは、ポインターの代わりに参照になる可能性があります (適切な測定のために)。 pData はすべての場合に必要であるため、内部でこれを行う理由はありません)。ループ。
控えめに言っても、コードは明らかに臭いです。オプティマイザがこれを行っていることを「非難」しようとしているわけではありません。しかし、私は尋ねています:私は何を間違っていますか? プログラムは醜いかもしれませんが、有効なコードです...
C++/CLI と v110_xp-Toolset で VS2012 を使用していることを付け加えておきます。最適化は /O2 に設定されています。また、問題を本当に再現したい場合 (それはこの質問のポイントではありません)、プログラムの複雑さをいじる必要があることにも注意してください。これは非常に単純化された例であり、オプティマイザーはポインターの割り当てを削除しないことがあります。関数の背後に隠れる&m_data
ことは「役立つ」ようです。
編集:
Q: コンパイラが提供されている例のように最適化していることをどのように知ることができますか?
A: 私はアセンブラを読むのが苦手ですが、アセンブラを見て、次のように動作していると思わせる 3 つの観察を行いました。
- 最適化が開始されるとすぐに (通常は割り当てを追加するとうまくいきます)、ポインターの割り当てには関連付けられたアセンブラー ステートメントがありません。また、宣言まで移動されていないため、実際には初期化されていないようです(少なくとも私には)。
- プログラムがクラッシュした場合、デバッガーは割り当てステートメントをスキップします。プログラムが問題なく実行される場合、デバッガーはそこで停止します。
pData
デバッグ中に の内容と の内容を見ると、 -branch 内のすべての割り当てが有効であり、正しい値を受け取るm_data
ことが明確に示されています。ポインター自体は、最初から持っていた初期化されていない同じ値をまだ指しています。したがって、実際にはポインターを使用して割り当てをまったく行っていないと想定する必要があります。if
m_data
m_data
Q: i (ループ展開) と何か関係がありますか?
A: いいえ、実際のプログラムは実際には do { ... } while() を使用して SQL SELECT 結果セットをループするため、反復カウントは完全にランタイム固有であり、コンパイラによって事前に決定することはできません。