どちらが速いか:スタック割り当てまたはヒープ割り当て


503

この質問はかなり初歩的に聞こえるかもしれませんが、これは私が一緒に働く別の開発者との議論でした。

ヒープに割り当てるのではなく、スタック割り当てに可能な限り注意を払いました。彼は私に話し、私の肩越しに見守っていて、彼らは同じパフォーマンスであるので、それは必要ではないとコメントしました。

スタックの増加は一定の時間であり、ヒープ割り当てのパフォーマンスは現在のヒープの複雑さ(適切なサイズのホールを見つける)と割り当て解除(ホールを縮小して断片化を減らす)の両方に依存するという印象を受けました。多くの標準ライブラリの実装では、私が間違っていない限り、削除中にこれを行うのに時間がかかります)。

これは、おそらくコンパイラに大きく依存するものとして私を襲います。特にこのプロジェクトでは、PPCアーキテクチャにMetrowerksコンパイラを使用しています。この組み合わせに関する洞察が最も役立ちますが、一般的に、GCCとMSVC ++の場合はどうなりますか?ヒープ割り当てはスタック割り当てよりもパフォーマンスが高くありませんか?違いはありませんか?または、違いが非常に小さいので、無意味なマイクロ最適化になります。


11
私はこれがかなり古いことを知っていますが、さまざまな種類の割り当てを示すいくつかのC / C ++スニペットを見るといいでしょう。
ジョセフワイズマン

42
あなたの牛のオーカーはひどく無知ですが、彼がひどく無知であることについて権威ある主張をするので、彼は危険です。チームからそのような人々をできるだけ早く切除してください。
ジムバルター2013年

5
ヒープは通常スタックよりはるかに大きいことに注意してください。大量のデータが割り当てられている場合は、実際にヒープに配置するか、OSからスタックサイズを変更する必要があります。
Paul Draper

1
すべての最適化は、ベンチマークや複雑さの引数がない限り、デフォルトでは無意味なマイクロ最適化です。
ビョルンLindqvist

2
あなたの同僚は主にJavaまたはC#の経験があるのでしょうか。これらの言語では、ほとんどすべてが内部でヒープに割り当てられているため、このような仮定につながる可能性があります。
Cort Ammon 2018

回答:


493

スタックの割り当ては、スタックポインタを移動するだけなので、はるかに高速です。メモリプールを使用すると、ヒープの割り当てから同等のパフォーマンスを得ることができますが、それにはわずかに複雑さが追加され、独自の問題が伴います。

また、スタックとヒープはパフォーマンスを考慮するだけではありません。また、オブジェクトの予想される寿命についても多くの情報を提供します。


211
そして、もっと重要なのは、スタックは、あなたが得るメモリは任意のfarヒープに割り当てられたメモリよりもキャッシュにあることがはるかに可能性があり、熱い常にある
ブノワ・

47
一部の(ほとんどが埋め込まれている、私が知っている)アーキテクチャでは、スタックは高速オンダイメモリ(SRAMなど)に格納される場合があります。これは大きな違いを生む可能性があります!
レアンダー

38
スタックは実際にはスタックなので。スタックが使用されていない限り、スタックで使用されているメモリを解放することはできません。管理はありません、あなたはそれに物事をプッシュまたはポップします。一方、ヒープメモリは管理されます。それはカーネルにメモリチャンクを要求し、おそらくそれらを分割し、それらをマージし、それらを再利用し、それらを解放します。スタックは、実際には高速で短い割り当てを目的としています。
ブノワ・

24
@Pacerierスタックはヒープよりもはるかに小さいため。大きな配列を割り当てる場合は、ヒープに割り当てる方が適切です。スタックに大きな配列を割り当てようとすると、スタックオーバーフローが発生します。たとえばC ++でこれを試してください:int t [100000000]; たとえば、t [10000000] = 10を試してください。そしてcout << t [10000000]; スタックオーバーフローが発生するか、機能せず、何も表示されません。ただし、配列をヒープに割り当てる場合:int * t = new int [100000000]; その後も同じ操作を実行すると、ヒープにそのような大きな配列に必要なサイズがあるため、機能します。
リリアンA.モラル

7
最も明白な理由@Pacerierスタック上のオブジェクトは、それらが割り当てられているブロックを出る際にスコープの外に出ていることである。
ジムBalter

166

スタックははるかに高速です。それは文字通り、ほとんどの場合、たとえばx86で、ほとんどのアーキテクチャで単一の命令のみを使用します。

sub esp, 0x10

(これにより、スタックポインターが0x10バイトだけ下に移動し、それによって変数で使用するためにそれらのバイトが「割り当てられます」。)

もちろん、スタックのサイズは非常に有限です。スタックの割り当てを使いすぎたり、再帰を試みたりするとすぐにわかります:-)

また、プロファイリングで実証されているように、それを検証可能に必要としないコードのパフォーマンスを最適化する理由はほとんどありません。「時期尚早の最適化」は、多くの場合、その価値よりも多くの問題を引き起こします。

私の経験則:コンパイル時にデータが必要になることがわかっていて、サイズが数百バイト未満の場合は、スタックに割り当てます。それ以外の場合は、ヒープに割り当てます。


20
1つの命令。これは通常、スタック上のすべてのオブジェクトによって共有されます。
MSalters 2008年

9
特に、検証可能にそれを必要とすることについて、ポイントを明確にしました。パフォーマンスに対する人々の懸念がどこにあるのか見落とされていることに私は絶えず驚かされます。
Mike Dunlavey、2009年

6
「割り当て解除」も非常にシンプルで、単一のleave命令で実行されます。
ドキュメント

15
ここで「隠された」コストを覚えておいてください。特に、スタックを初めて拡張するときは。これを行うと、ページフォールトが発生する可能性があり、メモリを割り当てる(または最悪の場合はスワップからロードする)ために何らかの作業を行う必要があるカーネルへのコンテキストスイッチが発生します。

2
場合によっては、0命令で割り当てることもできます。割り当てる必要のあるバイト数に関する情報がわかっている場合、コンパイラーは、他のスタック変数を割り当てると同時に、それらを事前に割り当てることができます。それらの場合、あなたはまったく何も支払いません!
Cort Ammon

119

正直なところ、パフォーマンスを比較するプログラムを書くのは簡単です。

#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つ以上を行います。

  1. データメンバーをに追加emptyし、ループでそのデータメンバーにアクセスします。しかし、データメンバーから読み取るだけの場合、オプティマイザーは定数の折りたたみを実行してループを削除できます。データメンバーのみに書き込む場合、オプティマイザーはループの最後の反復を除いてすべてスキップする可能性があります。さらに、問題は「スタックの割り当てとデータアクセスとヒープの割り当てとデータアクセス」ではありませんでした。

  2. 宣言e volatileしかしvolatile多くの場合、間違ってコンパイルされている(PDF)を。

  3. 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

2
また、メイン関数の最初に「キャリブレーション」ループを追加する必要があります。これにより、ループサイクルごとにどのくらいの時間が得られるかがわかり、他のループを調整して、サンプルが確実に実行されるようにします。使用している固定定数の代わりに、ある程度の時間。
ジョー・ピネダ

2
また、各オプションループの実行回数を増やして(さらに、g ++に最適化しないように指示して?)、大きな結果が得られてうれしいです。したがって、スタックが高速であるとは言い難い事実があります。あなたの努力をありがとう!
ジョー・ピネダ

7
このようなコードを取り除くのはオプティマイザの仕事です。オプティマイザーをオンにして、実際に最適化されないようにする正当な理由はありますか?わかりやすくするために回答を編集しました。オプティマイザとの戦いを楽しんでいる場合は、スマートコンパイラの作成者がいかに優れているかを学ぶ準備をしてください。
Max Lybbert 2009年

3
私は非常に遅いですが、ヒープ割り当てがカーネルを介してメモリを要求することをここで言及することも非常に価値があるため、パフォーマンスの打撃はカーネルの効率にも強く依存します。HRタイマーの変更やLinux(Linuxの3.10.7-ジェンツ#2 SMP水9月4日午後06時58分21秒MDT 2013 x86_64の)を使用してこのコードを使用して、各ループ1億回の反復を使用して、この性能を与える:stack allocation took 0.15354 seconds, heap allocation took 0.834044 seconds-O0セット、意思Linuxヒープの割り当ては、私の特定のマシンで約5.5の係数で遅くなるだけです。
Taywee 2013年

4
最適化のないウィンドウ(デバッグビルド)では、非デバッグヒープよりもはるかに遅いデバッグヒープが使用されます。オプティマイザを「だます」ことは悪い考えではないと思います。コンパイラー作成者は賢いですが、コンパイラーはAIではありません。
ポール、2014年

30

Xbox 360 Xenonプロセッサーでのスタックとヒープの割り当てについて興味深いことは、他のマルチコアシステムにも当てはまる可能性がありますが、ヒープに割り当てると、クリティカルセクションが入力されて他のすべてのコアが停止し、割り当てが行われます。矛盾しない。したがって、タイトなループでは、スタック割り当ては、ストールを防止するため、固定サイズの配列を実現する方法でした。

これは、マルチコア/マルチプロシージャ用にコーディングしている場合に検討すべきもう1つのスピードアップになる可能性があります。スタックの割り当ては、スコープ関数を実行しているコアのみが表示でき、他のコア/ CPUには影響しません。


4
これは、Xenonだけでなく、ほとんどのマルチコアマシンにも当てはまります。そのPPUコアで2つのハードウェアスレッドを実行している可能性があるため、Cellでもそれを行う必要があります。
Crashworks

15
これは、ヒープアロケータの(特に貧弱な)実装の影響です。より良いヒープアロケータは、すべての割り当てでロックを取得する必要はありません。
Chris Dodd、

19

非常にパフォーマンスの高い特定のサイズのオブジェクト用に、特別なヒープアロケーターを作成できます。ただし、一般的なヒープアロケーターは特にパフォーマンスが良いわけではありません。

また、オブジェクトの予想寿命についてTorbjörnGyllebringにも同意します。いい視点ね!


1
これは、スラブ割り当てと呼ばれることもあります。
Benoit

8

スタック割り当てとヒープ割り当ては、一般的に交換可能ではないと思います。また、両方の性能が一般的な使用に十分であることを願っています。

割り当ての範囲に適した小さいアイテムを強くお勧めします。大きなアイテムの場合、おそらくヒープが必要です。

複数のスレッドを持つ32ビットオペレーティングシステムでは、アドレススペースを分割する必要があるため、スタックはしばしば制限されます(ただし、通常は少なくとも数MBに制限されます)。シングルスレッドシステム(とにかくLinux glibcシングルスレッド)では、スタックはどんどん大きくなるため、制限ははるかに少なくなります。

64ビットオペレーティングシステムでは、スレッドスタックを非常に大きくするのに十分なアドレス空間があります。


6

通常、スタック割り当ては、スタックポインタレジスタからの減算で構成されます。これは、ヒープを検索するよりも高速です。

スタック割り当てでは、仮想メモリのページを追加する必要がある場合があります。ゼロ化されたメモリの新しいページを追加する場合、ディスクからページを読み取る必要がないため、通常はヒープを検索するよりも高速になります(特に、ヒープの一部もページアウトされている場合)。まれな状況で、そのような例を作成すると、すでにRAMにあるヒープの一部に十分なスペースがたまたまありますが、スタックに新しいページを割り当てると、他のページが書き出されるのを待つ必要がありますディスクに。そのまれな状況では、ヒープはより高速です。


ページングされない限り、ヒープは「検索」されるとは思わない。ソリッドステートメモリはマルチプレクサを使用し、メモリ、つまりランダムアクセスメモリに直接アクセスできます。
Joe Phillips、

4
ここに例があります。呼び出し側プログラムは37バイトの割り当てを要求します。ライブラリ関数は、少なくとも40バイトのブロックを探します。フリーリストの最初のブロックは16バイトです。フリーリストの2番目のブロックは12バイトです。3番目のブロックは44バイトです。ライブラリはその時点で検索を停止します。
Windowsプログラマ

6

ヒープ割り当てに比べて桁違いのパフォーマンスの利点は別として、長時間実行されるサーバーアプリケーションにはスタック割り当てが適しています。最高のマネージヒープでさえ、最終的には断片化され、アプリケーションのパフォーマンスが低下します。


4

スタックには容量の制限がありますが、ヒープには容量がありません。プロセスまたはスレッドの一般的なスタックは約8Kです。一度割り当てられたサイズは変更できません。

スタック変数はスコープ規則に従いますが、ヒープ変数は従いません。命令ポインタが関数を超えた場合、その関数に関連付けられているすべての新しい変数はなくなります。

最も重要なのは、全体的な関数呼び出しチェーンを事前に予測することができないことです。したがって、200バイトを割り当てるだけでスタックオーバーフローが発生する可能性があります。これは、アプリケーションではなくライブラリを作成する場合に特に重要です。


1
最新のOSでユーザーモードスタックに割り当てられる仮想アドレス空間の量は、デフォルトで少なくとも64kB以上になる可能性があります(Windowsでは1MB)。カーネルスタックサイズについて話していますか?
bk1e 2008年

1
私のマシンでは、プロセスのデフォルトのスタックサイズは8MBであり、KBではありません。お使いのコンピューターは何歳ですか?
グレッグロジャース

3

存続期間は非常に重要であり、割り当てられるものが複雑な方法で構築される必要があるかどうかと思います。たとえば、トランザクション駆動型モデリングでは、通常、トランザクション構造に入力して、一連のフィールドを操作関数に渡す必要があります。例については、OSCI SystemC TLM-​​2.0標準を参照してください。

これらをスタックに割り当てて操作の呼び出しの近くに配置すると、構築にコストがかかるため、非常に大きなオーバーヘッドが発生する傾向があります。ヒープに割り当て、トランザクションオブジェクトをプールするか、「このモジュールではトランザクションオブジェクトを1つだけ必要とする」などの単純なポリシーを使用して再利用することをお勧めします。

これは、各操作呼び出しでオブジェクトを割り当てるよりも何倍も高速です。

その理由は、オブジェクトの構造が高く、有効寿命がかなり長いためです。

私は言うでしょう:それは本当にあなたのコードの振る舞いに依存することができるので、両方を試して、あなたのケースで最もうまくいくものを見てください。


3

おそらく、ヒープ割り当てとスタック割り当ての最大の問題は、一般的な場合のヒープ割り当ては無制限の操作であるため、タイミングが問題となる場所では使用できないことです。

タイミングが問題にならない他のアプリケーションの場合、それほど重要ではないかもしれませんが、ヒープを大量に割り当てると、実行速度に影響します。スタックは、存続期間が短く頻繁に割り当てられるメモリ(たとえば、ループ内)に、できるだけ長く使用するようにしてください。アプリケーションの起動時にヒープ割り当てを行ってください。


3

より高速なのは、jsutスタック割り当てではありません。また、スタック変数の使用で多くの勝利を収めます。彼らはより良い参照の局所性を持っています。そして最後に、割り当て解除もはるかに安価です。


3

スタック割り当てはいくつかの命令ですが、私が知っている最速のRTOヒープアロケーター(TLSF)は平均で150命令程度を使用します。また、スタック割り当てはスレッドローカルストレージを使用するため、ロックを必要としません。これは別の大きなパフォーマンスの向上です。そのため、スタック割り当ては、環境のマルチスレッドの程度に応じて、2〜3桁速くなります。

一般に、パフォーマンスを重視する場合、ヒープの割り当ては最後の手段です。実行可能な中間オプションは、固定プールアロケーターにすることもできます。これも2、3の命令であり、割り当てごとのオーバーヘッドがほとんどないため、小さな固定サイズのオブジェクトに最適です。欠点は、固定サイズのオブジェクトでのみ機能し、本質的にスレッドセーフではなく、ブロックの断片化の問題があります。


3

C ++言語に固有の懸念事項

まず第一に、C ++によって義務付けられている、いわゆる「スタック」または「ヒープ」の割り当てはありません。ブロックスコープ内の自動オブジェクトについて話している場合、それらは「割り当て」られていません。(ところで、Cの自動ストレージ期間は「割り当てられた」とは完全に同じではありません。後者はC ++の用語では「動的」です。)動的に割り当てられたメモリは、「ヒープ」ではなく、フリーストアにありますが、後者は多くの場合(デフォルト)の実装です。

どおりが抽象マシンセマンティックルール、自動オブジェクトがまだメモリを占有し、準拠C ++の実装は、(それがプログラムの観察可能な振る舞いを変更しない場合)には、これは問題ではないことを証明することができたときに、この事実を無視することを許可されています。このアクセス許可は、ISO C ++ のas-ifルールによって付与されます。これは、通常の最適化を有効にする一般的な句でもあります(ISO Cにもほぼ同じルールがあります)。as-ifルールの他に、ISO C ++にはコピー省略ルールもあります。オブジェクトの特定の作成を省略できるようにするため。これにより、コンストラクタとデストラクタの呼び出しが省略されます。その結果、これらのコンストラクタとデストラクタの自動オブジェクト(存在する場合)も削除され、ソースコードで暗黙の単純な抽象セマンティクスと比較されます。

一方、無料のストア割り当ては、設計上、間違いなく「割り当て」です。ISO C ++ルールでは、そのような割り当ては割り当て関数の呼び出しによって実現できます。ただし、ISO C ++ 14以降、特定の場合にグローバル割り当て関数(つまり)呼び出しをマージできるようにする新しい(as-if-if)ルール::operator newあります。したがって、動的割り当て操作の一部は、自動オブジェクトの場合のように何もしない場合があります。

割り当て関数は、メモリのリソースを割り当てます。オブジェクトは、アロケーターを使用した割り当てに基づいてさらに割り当てることができます。自動オブジェクトの場合、それらは直接表示されます-基礎となるメモリにアクセスして他のオブジェクトに(配置によってnew)メモリを提供するために使用できますが、これはフリーストアとしては意味がありません。移動する方法がないためです。他のリソース。

他のすべての懸念はC ++の範囲外です。それにもかかわらず、それらは依然として重要な場合があります。

C ++の実装について

C ++は、具体化されたアクティベーションレコードや、ある種のファーストクラスの継続(たとえば、有名なによるcall/cc)を公開しません。アクティベーションレコードフレームを直接操作する方法はありません-実装が自動オブジェクトを配置する必要がある場合。基礎となる実装(インラインアセンブリコードなどの「ネイティブ」の非移植性コード)との(移植性のない)相互運用がなくなると、フレームの基礎となる割り当ての省略は非常に簡単になります。たとえば、呼び出された関数がインライン化されている場合、フレームは他のフレームに効果的にマージできるため、「割り当て」が何であるかを示す方法はありません。

ただし、相互運用性が尊重されると、状況は複雑になります。C ++の一般的な実装では、ネイティブ(ISAレベルのマシン)コードと共有されるバイナリ境界として、いくつかの呼び出し規約を使用してISA(命令セットアーキテクチャ)の相互運用機能を公開します。これは、特に、ISAレベルのレジスタによって(多くの場合、アクセスする特定のマシン命令によって)直接保持されるスタックポインタを維持する場合に、明らかにコストがかかります。スタックポインタは、(現在アクティブな)関数呼び出しの上部フレームの境界を示します。関数呼び出しが入力されると、新しいフレームが必要になり、スタックポインタが必要なフレームサイズ以上の値で(ISAの規則に応じて)追加または減算されます。フレームは割り当てられたと言われます操作後のスタックポインター。関数のパラメーターは、呼び出しに使用される呼び出し規約に応じて、スタックフレームにも渡すことができます。フレームは、C ++ソースコードで指定された自動オブジェクト(おそらくパラメーターを含む)のメモリを保持できます。このような実装の意味では、これらのオブジェクトは「割り当て」られます。コントロールが関数呼び出しを終了すると、フレームは不要になり、通常はスタックポインターを呼び出し前の状態(呼び出し規約に従って以前に保存された)に戻すことによって解放されます。これは「割り当て解除」と見なすことができます。これらの操作は、アクティブ化レコードを効果的にLIFOデータ構造にするため、「(呼び出し)スタック」と呼ばれることがよくあります

ほとんどのC ++実装(特にISAレベルのネイティブコードを対象とし、その即時出力としてアセンブリ言語を使用するもの)は、このような同様の戦略を使用しているため、このような紛らわしい「割り当て」方式が一般的です。このような割り当て(および割り当て解除)はマシンサイクルを費やしますが、(最適化されていない)呼び出しが頻繁に発生すると、一般的なコードパターン(たとえば、/ 命令の実装における スタックエンジン)。PUSHPOP

しかし、とにかく、一般に、スタックフレームの割り当てのコストは、フリーストアを操作する割り当て関数の呼び出し(完全に最適化されていない限り)比べて大幅に少ないことは事実です。 :-)スタックポインタやその他の状態を維持するための操作。通常、割り当て関数は、ホストされた環境によって提供されるAPI(OSによって提供されるランタイムなど)に基づいています。関数呼び出しの自動オブジェクトを保持する目的とは異なり、このような割り当ては汎用的であるため、スタックのようなフレーム構造にはなりません。従来は、ヒープ(または複数のヒープ)と呼ばれるプールストレージからスペースを割り当てます。「スタック」とは異なり、ここでの「ヒープ」の概念は、使用されているデータ構造を示すものではありません。それは数十年前の初期の言語実装から派生しています。(ところで、コールスタックは通常、プログラムまたはスレッドの起動時の環境によって、ヒープから固定またはユーザー指定のサイズで割り当てられます。)ユースケースの性質により、ヒープからの割り当てと割り当て解除は(プッシュまたはポップよりも)はるかに複雑になります。スタックフレーム)、ハードウェアで直接最適化することはほとんど不可能です。

メモリアクセスへの影響

通常のスタック割り当てでは常に新しいフレームが一番上に配置されるため、ローカリティは非常に良好です。これはキャッシュにやさしいです。OTOH、フリーストアにランダムに割り当てられたメモリには、そのようなプロパティはありません。ISO C ++ 17以降、によって提供されるプールリソーステンプレートがあります<memory>。このようなインターフェースの直接的な目的は、連続する割り当ての結果をメモリ内で互いに近づけることです。これは、この戦略が現代の実装でのパフォーマンスに一般的に優れているという事実を認めます。たとえば、現代のアーキテクチャでのキャッシュに友好的です。ただし、これは割り当てではなくアクセスのパフォーマンスに関するものです。

並行性

メモリへの同時アクセスの期待は、スタックとヒープ間で異なる影響を与える可能性があります。コールスタックは通常、C ++実装の1つの実行スレッドによって排他的に所有されます。OTOH、ヒープはプロセス内のスレッド間で共有されることがよくあります。このようなヒープの場合、割り当て関数と割り当て解除関数は、共有された内部管理データ構造をデータ競合から保護する必要があります。その結果、ヒープの割り当てと割り当て解除では、内部同期操作のために追加のオーバーヘッドが発生する可能性があります。

スペース効率

ユースケースと内部データ構造の性質上、スタックは内部メモリの断片化の影響を受けますが、スタックは影響を受けません。これはメモリ割り当てのパフォーマンスに直接影響しませんが、仮想メモリを備えたシステムでは、スペース効率が低いため、メモリアクセスの全体的なパフォーマンスが低下する可能性があります。HDDが物理メモリのスワップとして使用されている場合、これは特にひどいです。レイテンシが非常に長くなることがあり、場合によっては数十億サイクルになることもあります。

スタック割り当ての制限

スタック割り当ては、実際にはヒープ割り当てよりもパフォーマンスが優れていることがよくありますが、スタック割り当てが常にヒープ割り当てを置き換えることができるという意味ではありません。

まず、実行時にISO C ++で移植可能な方法で指定されたサイズでスタック上のスペースを割り当てる方法はありません。allocaおよびG ++のVLA(可変長配列)などの実装によって提供される拡張機能がありますが、それらを回避する理由があります。(IIRC、Linuxソースでは最近VLAの使用が削除されています。)(また、ISO C99ではVLAが義務付けられていますが、ISO C11ではサポートがオプションになっています。)

第2に、スタックスペースの枯渇を検出するための信頼性の高いポータブルな方法はありません。これは、しばしばスタックオーバーフロー(このサイトの語源)と呼ばれますが、おそらくより正確には、スタックオーバーランです。実際には、これにより無効なメモリアクセスが発生し、プログラムの状態が破損します(...または、さらに悪いことに、セキュリティホール)。実際、ISO C ++には「スタック」の概念がなく、リソースが使い果たされた場合の動作は未定義です。自動オブジェクトのためにどのくらいの余地を残すべきかについて注意してください。

スタックスペースが不足すると、スタックに割り当てられたオブジェクトが多すぎます。これは、関数のアクティブな呼び出しが多すぎるか、自動オブジェクトの不適切な使用が原因である可能性があります。このような場合は、バグの存在を示唆している可能性があります。たとえば、正しい終了条件のない再帰的な関数呼び出しです。

それでも、深い再帰呼び出しが必要になる場合があります。バインドされていないアクティブな呼び出しのサポートを必要とする言語の実装では(呼び出しの深さは合計メモリによってのみ制限されます)、通常のC ++実装のように、(現在の)ネイティブ呼び出しスタックをターゲット言語のアクティブ化レコードとして直接使用することはできません。この問題を回避するには、アクティベーションレコードを作成する別の方法が必要です。たとえば、SML / NJは明示的にフレームをヒープに割り当て、サボテンスタックを使用します。このようなアクティブ化レコードフレームの複雑な割り当ては、通常、呼び出しスタックフレームほど速くありません。ただし、そのような言語が適切に末尾再帰が保証されてさらに実装されている場合、オブジェクト言語での直接スタック割り当て(つまり、言語の「オブジェクト」は参照として保存されませんが、非共有C ++オブジェクトに1対1でマッピングできるネイティブプリミティブ値)は、さらに複雑になります。一般的にパフォーマンスのペナルティ。C ++を使用してそのような言語を実装する場合、パフォーマンスへの影響を見積もることは困難です。


stlと同様に、これらの概念を比較する意思はますます少なくなっています。cppcon2018の多くの人もheap頻繁に使用します。
陳力

@陳力「ヒープ」は、いくつかの特定の実装を念頭に置いていると明確になる可能性があるため、場合によっては問題ないかもしれません。ただし、「一般的に」冗長です。
FrankHB 2018年

相互運用とは何ですか?
陳力

@陳力つまり、インラインアセンブリコードなど、C ++ソースに関連するあらゆる種類の「ネイティブ」コードの相互運用を意味しました。これは、C ++でカバーされていない(ABIの)前提に依存しています。COM相互運用機能(一部のWindows固有のABIに基づく)はほぼ同じですが、ほとんどC ++に対して中立です。
FrankHB 2018年

2

このような最適化については、一般的な注意点があります。

得られる最適化は、プログラムカウンターが実際にそのコード内にある時間に比例します。

プログラムカウンターをサンプリングすると、それがどこで時間を費やしているかがわかります。これは通常、コードのごく一部にあり、多くの場合、ユーザーが制御できないライブラリルーチンにあります。

オブジェクトのヒープ割り当てに多くの時間を費やしていることがわかった場合にのみ、オブジェクトのスタック割り当てが著しく速くなります。


2

スタックアロケータがスタックベースの割り当て手法を使用することは確かに可能ですが、スタックの割り当てはほとんど常にヒープの割り当てと同じかそれよりも高速です。

ただし、スタックベースの割り当てとヒープベースの割り当ての全体的なパフォーマンスを処理する場合は、より大きな問題があります(または、ローカルの割り当てと外部の割り当てのほうが若干良い)。通常、ヒープ(外部)割り当ては、さまざまな種類の割り当てと割り当てパターンを処理するため、低速です。使用しているアロケータのスコープを小さくする(アルゴリズム/コードに対してローカルにする)と、大きな変更なしにパフォーマンスが向上する傾向があります。たとえば、割り当てと割り当て解除のペアにLIFOの順序付けを強制するなど、割り当てパターンにより良い構造を追加すると、アロケーターをよりシンプルで構造化された方法で使用することにより、アロケーターのパフォーマンスを向上させることもできます。または、特定の割り当てパターンに合わせて調整されたアロケータを使用または記述できます。ほとんどのプログラムは、いくつかの個別のサイズを頻繁に割り当てます。そのため、いくつかの固定(できれば既知の)サイズのlookasideバッファーに基づくヒープは、非常にうまく機能します。Windowsは、まさにこの理由から、断片化の少ないヒープを使用しています。

一方、スレッドが多すぎると、32ビットのメモリ範囲でのスタックベースの割り当ても危険にさらされます。スタックは連続したメモリ範囲を必要とするため、スレッドが多いほど、スタックオーバーフローなしで実行するために必要な仮想アドレス空間が多くなります。これは(今のところ)64ビットの問題ではありませんが、多くのスレッドを使用する長時間実行プログラムで大混乱を引き起こす可能性があります。断片化が原因で仮想アドレス空間が不足することは、常に対処するのが困難です。


私はあなたの最初の文に同意しません。
ブライアンビーニング、16

2

他の人が言ったように、スタックの割り当ては一般的にはるかに高速です。

ただし、オブジェクトのコピーに負荷がかかる場合は、スタックに割り当てると、後でオブジェクトを使用するときに注意しないと、パフォーマンスが大幅に低下する可能性があります。

たとえば、スタックに何かを割り当て、それをコンテナーに入れる場合は、ヒープに割り当て、ポインターをコンテナーに格納する(たとえば、std :: shared_ptr <>を使用)方がよいでしょう。値でオブジェクトを渡したり返したりする場合や、他の同様のシナリオでも同じことが当てはまります。

重要なのは、多くの場合、スタック割り当ては通常ヒープ割り当てよりも優れていますが、計算モデルに最適でないときにスタック割り当てに手を出さないと、解決できない問題が発生する可能性があるということです。


2
class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

asmでこのようになります。にいるときfuncf1andポインターf2はスタック(自動ストレージ)に割り当てられています。ちなみに、Foo f1(a1)はスタックポインター(esp)に命令の影響を与えません。割り当てられているfuncため、メンバーを取得したい場合f1、その命令は次のようになりますlea ecx [ebp+f1], call Foo::SomeFunc()。スタックが割り当てもう一つは、誰かがメモリのようなものだと思うことがありFIFOFIFOあなたが機能している場合、一部の機能にアクセスし、のようなものを割り当てる際に起こったint i = 0、何のプッシュが起こっていません。


1

スタックの割り当ては単にスタックポインターを移動すること、つまり、ほとんどのアーキテクチャで単一の命令であることは前に述べました。ヒープ割り当ての場合に一般的に起こることと比較してください。

オペレーティングシステムは、空きメモリの一部を、空き部分の開始アドレスへのポインタと空き部分のサイズで構成されるペイロードデータを含むリンクリストとして維持します。Xバイトのメモリを割り当てるには、リンクリストをたどり、各ノートに順番にアクセスして、そのサイズが少なくともXかどうかを確認します。サイズP> = Xの部分が見つかると、Pは2つの部分に分割されます。サイズXおよびPX。リンクリストが更新され、最初の部分へのポインターが返されます。

ご覧のとおり、ヒープの割り当ては、要求しているメモリの量、メモリの断片化の程度などの要因によって異なります。


1

上記のほぼすべての回答で述べたように、一般的にスタック割り当てはヒープ割り当てよりも高速です。スタックのプッシュまたはポップはO(1)ですが、ヒープの割り当てまたは解放には、以前の割り当てのウォークが必要になる場合があります。ただし、通常、タイトでパフォーマンス集約的なループに割り当てることはできません。そのため、通常、選択は他の要因に行き着きます。

この区別をするのが良いかもしれません:ヒープで「スタックアロケータ」を使用できます。厳密に言うと、スタック割り当ては、割り当ての場所ではなく、実際の割り当て方法を意味します。実際のプログラムスタックに多くのものを割り当てる場合は、さまざまな理由でそれが悪い可能性があります。一方、スタックメソッドを使用してヒープに割り当てることが可能な場合は、割り当て方法として最良の選択です。

MetrowerksとPPCについて言及されたので、Wiiのことだと思います。この場合、メモリは貴重であり、可能な限りスタック割り当て方法を使用すると、フラグメントでメモリを無駄にしないことが保証されます。もちろん、これを行うには、「通常の」ヒープ割り当て方法よりも多くの注意が必要です。各状況のトレードオフを評価するのが賢明です。


1

考慮事項は通常、スタックとヒープの割り当てを選択するときの速度とパフォーマンスについてではないことに注意してください。スタックはスタックのように機能します。つまり、ブロックを押して再度ポップするのに適しています。プロシージャの実行もスタックに似ており、最後に入力されたプロシージャが最初に終了します。ほとんどのプログラミング言語では、プロシージャで必要なすべての変数は、プロシージャの実行中にのみ表示されるため、プロシージャに入るときにプッシュされ、終了またはリターン時にスタックからポップされます。

次に、スタックを使用できない例を示します。

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

手順Sでメモリを割り当て、それをスタックに置いてからSを終了すると、割り当てられたデータがスタックからポップされます。しかし、Pの変数xもそのデータを指していたため、xは不明な内容でスタックポインターの下のある場所を指します(スタックが下向きに成長すると想定)。スタックポインターが下にあるデータをクリアせずに上に移動した場合、コンテンツはまだ存在する可能性がありますが、スタックに新しいデータの割り当てを開始すると、ポインターxが実際にその新しいデータを指す場合があります。


0

他のアプリケーションのコードと使用法が関数に影響を与える可能性があるため、時期尚早の想定をしないでください。したがって、機能を確認することは分離には意味がありません。

アプリケーションに真剣に取り組んでいる場合は、それをVTuneするか、同様のプロファイリングツールを使用してホットスポットを確認します。

ケタン


-1

GCCで実際に生成されたコード(VSも覚えています)には、スタック割り当てを行うオーバーヘッドがありません

次の関数について言います:

  int f(int i)
  {
      if (i > 0)
      {   
          int array[1000];
      }   
  }

以下はコード生成です:

  __Z1fi:
  Leh_func_begin1:
      pushq   %rbp
  Ltmp0:
      movq    %rsp, %rbp
  Ltmp1:
      subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
  Ltmp2:
      movl    %edi, -4(%rbp)
      movl    -8(%rbp), %eax
      addq    $3880, %rsp
      popq    %rbp
      ret 
  Leh_func_end1:

したがって、ローカル変数がいくらあっても(ifまたはswitchの内部であっても)、3880だけが別の値に変更されます。ローカル変数がない場合を除き、この命令を実行するだけで済みます。したがって、ローカル変数の割り当てにはオーバーヘッドがありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.