C++プログラミング言語第4版、225ページ:コンパイラがコードを並べ替える結果が単純な実行順序と同じである限り、パフォーマンスを向上させるため。一部のコンパイラ、たとえばリリースモードのVisual C++は、このコードを並べ替えます。
#include <time.h>
...
auto t0 = clock();
auto r = veryLongComputation();
auto t1 = clock();
std::cout << r << " time: " << t1-t0 << endl;
この形に:
auto t0 = clock();
auto t1 = clock();
auto r = veryLongComputation();
std::cout << r << " time: " << t1-t0 << endl;
これにより、元のコードとは異なる結果が保証されます(ゼロと報告されたゼロ時間よりも大きい)。詳細な例については 私のその他の質問 を参照してください。この動作はC++標準に準拠していますか?
コンパイラは2つのclock
呼び出しを交換できません。 _t1
_は_t0
_の後に設定する必要があります。どちらの呼び出しも観察可能な副作用です。コンパイラーは、観測が抽象マシンの可能な観測と一致している限り、それらの観測可能な効果の間で、さらには観測可能な副作用を超えて何でも並べ替えることができます。
C++抽象マシンは正式には有限速度に制限されていないため、ゼロ時間でveryLongComputation()
を実行できます。実行時間自体は、観察可能な影響として定義されていません。実際の実装はこれと一致する場合があります。
心に留めておいてください。この答えの多くはC++標準notに依存しています。コンパイラに制限を課します。
まあ、_Subclause 5.1.2.3 of the C Standard [ISO/IEC 9899:2011]
_と呼ばれるものがあります:
抽象マシンでは、すべての式がセマンティクスで指定されたとおりに評価されます。実際の実装では、その値が使用されておらず、必要な副作用(関数の呼び出しや揮発性オブジェクトへのアクセスによって引き起こされるものも含む)が生成されていないと推定できる場合、式の一部を評価する必要はありません。
したがって、私は本当にこの動作-あなたが記述したもの-が標準に準拠していると疑っています。
さらに、再編成は実際に計算結果に影響を与えますが、コンパイラの観点から見ると、int main()
の世界に存在し、時間測定を行っているときに、覗き込んでカーネルに要求します。それは現在の時間であり、外の世界の実際の時間が重要ではないメインの世界に戻ります。 clock()自体はプログラムと変数に影響せず、プログラムの動作はそのclock()関数に影響しません。
クロック値はそれらの間の差を計算するために使用されます-それはあなたが求めたものです。 2つの測定の間に何かが起こっている場合、要求されたのはクロックの差であり、測定間のコードがプロセスとしての測定に影響を与えないため、コンパイラーの観点からは関係ありません。
ただし、これによって、説明されている動作が非常に不快であるという事実は変わりません。
不正確な測定は不快ですが、さらに悪化し、危険にさえなる可能性があります。
このサイト から取得した次のコードを考えてみます。
_void GetData(char *MFAddr) {
char pwd[64];
if (GetPasswordFromUser(pwd, sizeof(pwd))) {
if (ConnectToMainframe(MFAddr, pwd)) {
// Interaction with mainframe
}
}
memset(pwd, 0, sizeof(pwd));
}
_
正常にコンパイルされた場合はすべて問題ありませんが、最適化が適用されると、memset呼び出しが最適化され、深刻なセキュリティ上の欠陥が生じる可能性があります。なぜ最適化されるのですか?とてもシンプルです。変数pwd
は後で使用されず、プログラム自体には影響しないため、コンパイラーは再びmain()
の世界を考慮し、memsetをデッドストアと見なします。
はい、それは正当です-ifコンパイラは、clock()
呼び出しの間に発生するコード全体を確認できます。
veryLongComputation()
が内部的に不透明な関数呼び出しを実行する場合、コンパイラはその副作用がclock()
の副作用と交換可能であることを保証できないため、そうではありません。
そうでなければ、はい、それは交換可能です。
これは、時間が一流の実体ではない言語を使用するために支払う価格です。
割り当て関数は別の翻訳単位で定義でき、現在の翻訳単位が既にコンパイルされるまでコンパイルできないため、メモリ割り当て(new
など)がこのカテゴリに該当する場合があることに注意してください。したがって、メモリを割り当てるだけの場合、コンパイラは、割り当てと割り当て解除を、すべてのclock()
、メモリバリア、およびその他すべての最悪の場合のバリアとして扱うように強制されます。メモリアロケータ。これが不要であることを証明できます。実際には、コンパイラが実際にアロケータコードを調べてこれを証明しようとは思わないため、これらのタイプの関数呼び出しは実際にはバリアとして機能します。
少なくとも私の読書では、いいえ、これは許可されていません。標準からの要件は(§1.9/ 14)です。
全式に関連付けられているすべての値の計算と副作用は、評価される次の全式に関連付けられているすべての値の計算と副作用の前にシーケンスされます。
"as-if"ルール(§1.9/ 1)で定義される、コンパイラーがそれを超えて自由に並べ替える度合い:
この国際規格は、適合実装の構造に要件を課していません。特に、抽象マシンの構造をコピーまたはエミュレートする必要はありません。むしろ、以下で説明するように、抽象マシンの観察可能な動作を(のみ)エミュレートするには、準拠する実装が必要です。
これには、問題の動作(cout
によって書き込まれた出力)が公式に観察可能な動作であるかどうかという問題が残ります。簡単に言えば、そうです(§1.9/ 8)。
適合実装の最小要件は次のとおりです。
[...]
—プログラムの終了時に、ファイルに書き込まれたすべてのデータは、抽象的なセマンティクスに従ってプログラムを実行した場合に生じる可能性のある結果の1つと同じでなければなりません。
少なくとも私が読んだように、それはclock
への呼び出しが長い計算の実行と比較して並べ替えられる可能性があることを意味します。
ただし、正しい動作を保証するために追加の手順を実行したい場合は、他の1つの規定を利用することもできます(同じく1.9/8)。
—揮発性オブジェクトへのアクセスは、抽象マシンのルールに従って厳密に評価されます。
これを利用するには、コードを少し変更して次のようにします。
auto volatile t0 = clock();
auto volatile r = veryLongComputation();
auto volatile t1 = clock();
さて、標準の3つの別々のセクションに基づいて結論を下す必要はなく、かなり確実な回答しかなく、正確に1つの文を見て、絶対に特定の答え-このコードでは、clock
の使用を並べ替えるのと比較して、長い計算は明らかに禁止されています。
シーケンスがループ内にあり、veryLongComputation()がランダムに例外をスローするとします。次に、t0とt1はいくつ計算されますか?ランダム変数を事前に計算し、事前計算に基づいて並べ替えますか?
コンパイラは、メモリの読み取りだけが共有メモリからの読み取りであることを認識できるほどスマートですか?読み取り値は、原子炉内で制御棒がどれだけ移動したかを示す尺度です。クロックコールは、それらが移動する速度を制御するために使用されます。
または、タイミングがハッブル望遠鏡の鏡の研削を制御しているのかもしれません。笑
クロックの呼び出しを移動することは、コンパイラの作成者の決定に委ねるには危険すぎるようです。したがって、それが合法である場合、おそらく標準に欠陥があります。
IMO。