正直なところ、パフォーマンスを比較するプログラムを作成するのは簡単です。
#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
、正しくコンパイルされないことがよくあります (PDF)。volatile
ループ内のアドレスを取得します (別のファイルで宣言および定義されe
ている変数に割り当てます)。extern
しかし、この場合でも、コンパイラは、少なくともスタック上でe
は常に同じメモリ アドレスに割り当てられ、上記の (1) のように定数の折りたたみを行うことに気付く場合があります。ループのすべての繰り返しを取得しますが、オブジェクトが実際に割り当てられることはありません。
明らかなことを超えて、このテストは割り当てと解放の両方を測定するという点で欠陥があり、元の質問では解放について尋ねていませんでした。もちろん、スタックに割り当てられた変数はそのスコープの最後で自動的に割り当て解除されるため、呼び出さないdelete
と (1) 数値が歪められます (スタックの割り当て解除はスタック割り当てに関する数値に含まれるため、ヒープの割り当て解除を測定するのは公平です) および ( 2) 新しいポインタへの参照を保持し、delete
時間測定を取得した後に呼び出さない限り、かなりひどいメモリ リークが発生します。
私のマシンでは、Windows で g++ 3.4.4 を使用して、100000 未満の割り当てに対してスタックとヒープの両方の割り当てで「0 クロック ティック」を取得し、それでもスタック割り当てで「0 クロック ティック」を取得し、「15 クロック ティック」を取得します。 " ヒープ割り当て用。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