正直なところ、パフォーマンスを比較するプログラムを書くのは簡単です。
#include <ctime>
#include <iostream>
namespace {
class empty { }; // even empty classes take up 1 byte of space, minimum
}
int main()
{
std::clock_t start = std::clock();
for (int i = 0; i < 100000; ++i)
empty e;
std::clock_t duration = std::clock() - start;
std::cout << "stack allocation took " << duration << " clock ticks\n";
start = std::clock();
for (int i = 0; i < 100000; ++i) {
empty* e = new empty;
delete e;
};
duration = std::clock() - start;
std::cout << "heap allocation took " << duration << " clock ticks\n";
}
愚かな一貫性は小さな心のホブゴブリンだと言われています。どうやら最適化コンパイラーは、多くのプログラマーの心のホブゴブリンです。以前はこのディスカッションが答えの一番下にありましたが、人々は明らかにそれまで読むのに煩わされないので、すでに答えた質問を取得しないようにここに移動します。
最適化コンパイラは、このコードが何もしないことに気づき、それをすべて最適化する場合があります。そのようなことをするのはオプティマイザの仕事であり、オプティマイザとの戦いは愚か者の用事です。
現在使用されている、または将来使用されるすべてのオプティマイザをだますための良い方法がないため、最適化をオフにしてこのコードをコンパイルすることをお勧めします。
オプティマイザをオンにして、それとの戦いについて不平を言う人は誰でも公のあざけりの対象となるはずです。
ナノ秒の精度を気にするなら、私は使用しませんstd::clock()
。博士論文として結果を公開したい場合は、これについてもっと大きなことを行います。おそらく、GCC、Tendra / Ten15、LLVM、Watcom、Borland、Visual C ++、Digital Mars、ICC、その他のコンパイラを比較します。現状では、ヒープの割り当てはスタックの割り当てよりも数百倍も長くかかります。質問をさらに調査することに役立つものは何もありません。
オプティマイザには、テストしているコードを取り除く使命があります。オプティマイザーを実行するように指示し、オプティマイザーをだまして実際には最適化しないようにする理由は何もありません。しかし、それを行うことに価値を見た場合、私は次の1つ以上を行います。
データメンバーをに追加empty
し、ループでそのデータメンバーにアクセスします。しかし、データメンバーから読み取るだけの場合、オプティマイザーは定数の折りたたみを実行してループを削除できます。データメンバーのみに書き込む場合、オプティマイザーはループの最後の反復を除いてすべてスキップする可能性があります。さらに、問題は「スタックの割り当てとデータアクセスとヒープの割り当てとデータアクセス」ではありませんでした。
宣言e
volatile
、しかしvolatile
多くの場合、間違ってコンパイルされている(PDF)を。
e
ループ内のアドレスを取得します(extern
別のファイルで宣言および定義されている変数に割り当てることもできます)。ただし、この場合でも、コンパイラは、少なくともスタック上でe
は常に同じメモリアドレスに割り当てられ、上記の(1)のように定数の折りたたみを行うことに気づく場合があります。ループのすべての反復を取得しますが、オブジェクトが実際に割り当てられることはありません。
明らかなことを超えて、このテストは割り当てと割り当て解除の両方を測定するという点で欠陥があり、元の質問は割り当て解除について尋ねていませんでした。もちろん、スタックに割り当てられた変数は、スコープの最後で自動的に割り当て解除されます。したがって、呼び出しdelete
は(1)数値を歪めます(スタック割り当て解除はスタック割り当てに関する数値に含まれるため、ヒープ割り当て解除を測定するのは公正です)および( 2)新しいポインターへの参照を保持し、delete
時間の測定後に呼び出しを行わない限り、かなり悪いメモリリークを引き起こします。
私のマシンでは、Windowsのg ++ 3.4.4を使用して、スタックとヒープの両方に100000未満の割り当てで「0クロックティック」を取得しています。 "ヒープ割り当て用。10,000,000の割り当てを測定すると、スタックの割り当てには31クロックティック、ヒープの割り当てには1562クロックティックがかかります。
はい、最適化コンパイラは空のオブジェクトの作成を省略できます。私が正しく理解していれば、最初のループ全体が省略されることもあります。反復を10,000,000に増やすと、スタック割り当てには31クロックティック、ヒープ割り当てには1562クロックティックがかかりました。実行可能ファイルを最適化するようにg ++に指示しないと、g ++はコンストラクターを回避しなかったと言っても安全だと思います。
私がこれを書いて以来、スタックオーバーフローは、最適化されたビルドからパフォーマンスをポストすることを優先してきました。一般的に、これは正しいと思います。ただし、実際にコードを最適化したくない場合に、コードを最適化するようコンパイラーに要求するのは愚かです。バレーパーキングの追加料金に非常に似ているが、鍵の受け渡しを拒否しているように思える。この特定のケースでは、オプティマイザを実行したくありません。
わずかに変更されたバージョンのベンチマークを使用して(元のプログラムがループを通じて毎回スタックに何かを割り当てなかった有効なポイントに対処するため)、最適化せずにコンパイルしますが、リリースライブラリにリンクします(有効なポイントに対処するため)デバッグライブラリへのリンクによって引き起こされるスローダウンを含めたくない):
#include <cstdio>
#include <chrono>
namespace {
void on_stack()
{
int i;
}
void on_heap()
{
int* i = new int;
delete i;
}
}
int main()
{
auto begin = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; ++i)
on_stack();
auto end = std::chrono::system_clock::now();
std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());
begin = std::chrono::system_clock::now();
for (int i = 0; i < 1000000000; ++i)
on_heap();
end = std::chrono::system_clock::now();
std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
return 0;
}
表示:
on_stack took 2.070003 seconds
on_heap took 57.980081 seconds
私のシステムでは、コマンドラインでコンパイルしたときcl foo.cc /Od /MT /EHsc
。
最適化されていないビルドを取得するための私のアプローチに同意できない場合があります。それで結構です。自由にベンチマークを自由に変更してください。最適化をオンにすると、次のようになります。
on_stack took 0.000000 seconds
on_heap took 51.608723 seconds
スタックの割り当てが実際に瞬時に行われるのではなく、半端なコンパイラが、on_stack
何も役に立たないことに気づき、最適化することができるためです。私のLinuxラップトップ上のGCCもon_heap
、何も役に立たないことに気付き、それを最適化します。
on_stack took 0.000003 seconds
on_heap took 0.000002 seconds