私はこれが一般的な質問のように聞こえることを知っており、多くの同様の質問(こことウェブの両方)を見てきましたが、どれも私のジレンマに本当に似ていません。
このコードがあるとしましょう:
void GetSomeData(char* buffer)
{
// put some data in buffer
}
int main()
{
char buffer[1024];
while(1)
{
GetSomeData(buffer);
// do something with the data
}
return 0;
}
Buffer [1024]をグローバルに宣言した場合、パフォーマンスは向上しますか?
Timeコマンドを使用してunixでいくつかのテストを実行しましたが、実行時間に実質的な違いはありません。
しかし、私は本当に確信していません...
理論的には、この変化は違いを生むはずですか?
スタック内のデータよりもヒープ内のデータに速くアクセスしていますか?
本質的ではない...私が今まで取り組んだすべてのアーキテクチャでは、すべてのプロセス「メモリ」は、CPUキャッシュのレベルに基づいて同じ速度のセットで動作することが期待できます/ RAM = /スワップファイルは現在のデータを保持しており、そのメモリに対する操作がトリガーして他のプロセスから見えるようにしたり、他のプロセス/ CPU(コア)の変更を組み込んだりするハードウェアレベルの同期遅延。
OS(ページフォールト/スワッピングを担当)、およびスワップアウトされたページまたはまだアクセスされていないページへのアクセスをトラップするハードウェア(CPU)は、どのページが「スタック」対「ヒープ」であるかさえ追跡しません。 ..メモリページはメモリページです。とはいえ、グローバルデータの仮想アドレスはコンパイル時に計算およびハードコーディングできる可能性があり、スタックベースのデータのアドレスは通常スタックポインター相対であり、ヒープ上のメモリはほとんど常にポインターを使用してアクセスする必要があり、一部のシステムでは少し遅くなります-CPUのアドレス指定モードとサイクルに依存しますが、ほとんど常に重要ではありません-100万分の1が非常に重要である何かを書いている場合を除き、一見したり考え直したりする価値はありません。
とにかく、あなたの例では、グローバル変数を関数ローカル(スタック/自動)変数と比較しています...ヒープは関係していません。ヒープメモリは、new
またはmalloc
/realloc
から取得されます。ヒープメモリの場合、注目に値するパフォーマンスの問題は、アプリケーション自体がどのアドレスでどのくらいのメモリが使用されているかを追跡していることです。メモリへのポインタとしてnew
/malloc
/realloc
、およびポインターがdelete
dまたはfree
dであるため、さらに更新する時間が必要です。
グローバル変数の場合、メモリの割り当てはコンパイル時に効果的に行われますが、スタックベースの変数の場合、通常、ローカル変数(および一部のハウスキーピングデータ)のサイズのコンパイル時計算合計によって増加するスタックポインターがあります。関数が呼び出されます。したがって、main()
が呼び出されると、スタックポインタを変更する時間がありますが、buffer
がない場合は変更されず、ある場合は変更されるのではなく、異なる量だけ変更される可能性がありますですから、実行時のパフォーマンスにまったく違いはありません。
Jeff Hillの答え からの引用:
スタックは高速です。これは、アクセスパターンによりメモリの割り当てと割り当て解除が簡単になるためです(ポインタ/整数は単純にインクリメントまたはデクリメントされます)。ヒープには、割り当てまたは解放に関連するはるかに複雑な簿記があります。また、スタック内の各バイトは非常に頻繁に再利用される傾向があるため、プロセッサのキャッシュにマップされる傾向があり、非常に高速になります。ヒープのもう1つのパフォーマンスヒットは、ほとんどがグローバルリソースであるヒープがマルチスレッドで安全である必要があることです。つまり、各割り当ておよび割り当て解除は、プログラム内の他の「すべての」ヒープアクセスと同期する必要があります。
あなたの質問には実際には答えがありません。他に何をしているかに依存します。一般的に、ほとんどのマシンはプロセス全体で同じ「メモリ」構造を使用するため、変数が存在する場所(ヒープ、スタック、またはグローバルメモリ)に関係なく、アクセス時間は同じです。一方、最新のマシンのほとんどは、メモリパイプライン、複数レベルのキャッシュ、メインメモリ、仮想メモリを備えた階層メモリ構造を備えています。プロセッサで以前に何が行われたかに応じて、実際のアクセスはこれらのいずれか(ヒープ、スタック、またはグローバルに関係なく)であり、ここでのアクセス時間は、メモリが1つのクロックの場合、システムがディスク上の仮想メモリに移動する必要がある場合は、パイプラインの適切な場所で、約10ミリ秒程度まで。
すべての場合において、キーはローカリティです。アクセスが以前のアクセスの「近く」にある場合、たとえば、キャッシュなど、より高速な場所の1つでアクセスを見つける可能性が大幅に向上します。これに関しては、関数の引数にアクセスすると、スタックメモリ(少なくともIntel 32ビットプロセッサを使用して、少なくともより優れた設計のプロセッサを使用するため、引数はレジスタにある可能性が高い)。しかし、これはおそらくアレイが関係している場合には問題になりません。
価値があるのは、配列がスタック上にあるときとヒープ上にあるとき(GCC、Windows 10、-O3フラグ)、再起動直後(ヒープの断片化が最小化された場合):
const int size = 100100100;
int vals[size]; // STACK
// int *vals = new int[size]; // HEAP
startTimer();
for (int i = 1; i < size; ++i) {
vals[i] = vals[i - 1];
}
stopTimer();
std::cout << vals[size - 1];
// delete[] vals; // HEAP
もちろん、最初にスタックサイズを400 MBに増やす必要がありました。コンパイラがすべてを最適化しないようにするには、最後の要素を最後に印刷する必要があることに注意してください。
スタックにバッファを割り当てる場合、最適化の範囲はメモリにアクセスするコストではなく、ヒープ上の非常に高価な動的メモリ割り当ての多くを排除することです(スタックバッファ割り当ては、スタック全体がスレッドの起動時に割り当てられるため、瞬時と見なすことができます) 。
このトピックに関するブログ投稿が利用可能です stack-allocation-vs-heap-allocation-performance-benchmark 割り当て戦略のベンチマークを示しています。テストはCで記述され、純粋な割り当て試行とメモリ初期化による割り当ての比較を実行します。異なる合計データサイズで、ループの数が実行され、時間が測定されます。各割り当ては、異なるサイズの10個の異なるalloc/init/freeブロックで構成されます(合計サイズはグラフに表示されます)。
テストは、Intel(R)Core(TM)i7-6600U CPU、Linux 64ビット、4.15.0-50-generic、SpectreおよびMeltdownパッチを無効にして実行されます。
その結果、データを初期化しない場合の純粋な割り当てには大きな違いがあることがわかります。スタックはヒープより高速ですが、ループカウントが非常に多いことに注意してください。
割り当てられたデータが処理されているとき、スタックとヒープのパフォーマンスのギャップは減少するようです。 1Mのmalloc/init/free(またはstack alloc)ループで、各ループで10回の割り当てが試行されると、スタックは合計時間でヒープの8%だけ進んでいます。