web-dev-qa-db-ja.com

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

この質問はかなり初歩的に思えるかもしれませんが、これは私が一緒に働いている他の開発者と私が持っていた議論です。

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

私はいつもスタックの成長は一定の時間であり、ヒープ割り当てのパフォーマンスは割り当て(適切なサイズの穴を見つける)と割り当て解除(断片化を減らすために穴を折りたたむ)の両方の複雑さに依存していました。私が間違っていなければ、多くの標準的なライブラリ実装は削除の間にこれをするのに時間がかかります)。

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

477
Adam

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

また、スタック対ヒープはパフォーマンス上の考慮事項だけではありません。また、オブジェクトの予想寿命についても多くのことを伝えています。

471

スタックはずっと速いです。それは文字通りほとんどのアーキテクチャで、ほとんどの場合、単一の命令を使うだけです。 x86の場合

sub esp, 0x10

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

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

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

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

163
Dan Lenski

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

#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. evolatileを宣言します ただし、volatileは間違ってコンパイルされることがよくあります (PDF)。

  3. ループ内でeのアドレスを取得します(そして、それをexternとして宣言され、別のファイルで定義されている変数に代入します)。しかし、この場合でも、コンパイラは - 少なくともスタック上で - eが常に同じメモリアドレスに割り当てられ、上記の(1)のように定数の畳み込みを行うことに気付くかもしれません。ループのすべての反復を取得しましたが、オブジェクトは実際には割り当てられません。

明白なことを超えて、このテストは割り当てと割り当て解除の両方を測定するという点で欠陥があり、元の質問は割り当て解除について尋ねませんでした。もちろん、スタックに割り当てられた変数は自動的にスコープの最後で割り当て解除されるので、deleteを呼び出さないと(1)番号が歪められます(スタック割り当て解除はスタック割り当てに関する数に含まれるため、ヒープ割り当て解除を測定するのは公正です)。 (2)新しいポインタへの参照を保持し、時間を測定した後にdeleteを呼び出さない限り、かなり悪いメモリリークを引き起こします。

私のマシンでは、Windows上でg ++ 3.4.4を使用していますが、100000未満の割り当てではスタックとヒープの両方の割り当てに対して "0 clock ticks"が得られます。 "ヒープ割り当て用。 10,000,000の割り当てを測定すると、スタック割り当てには31クロックティックがかかり、ヒープ割り当てには1562クロックティックがかかります。


はい、最適化コンパイラは空のオブジェクトを作成しなくてもかまいません。私が正しく理解していれば、最初のループ全体を回避することすら可能です。繰り返し回数を10,000,000に増やしたとき、スタック割り当ては31クロックティック、ヒープ割り当ては1562クロックティックでした。 g ++に実行可能ファイルを最適化するように指示しない限り、g ++はコンストラクタを排除しなかったと言っても差し支えないと思います。


私がこれを書いてから何年もの間、Stack Overflowの好みは最適化されたビルドからパフォーマンスをポストすることでした。一般に、これは正しいと思います。しかし、実際にコードを最適化したくない場合は、コンパイラにコードの最適化を依頼するのは愚かなことです。それは私をバレットパーキングのために追加料金を払うことに非常に似ているが、キーを引き渡すことを拒否することとして私を襲います。この場合、オプティマイザを実行したくありません。

ベンチマークのわずかに変更されたバージョンを使用し(元のプログラムがループを通じて毎回スタック上に何かを割り当てなかったという有効な点に対処するため)、最適化せずにコンパイルし、リリースライブラリにリンクします。デバッグライブラリへのリンクが原因で発生する速度低下を含めないでください。

#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
115
Max Lybbert

Xbox 360 Xenonプロセッサでのスタックとヒープの割り当てについて私が学んだ興味深いことは、他のマルチコアシステムにも当てはまるかもしれませんが、ヒープで割り当てるとクリティカルセクションに入って他のすべてのコアを停止することです。衝突しないでください。そのため、タイトなループでは、スタック割り当てはストールを防止するため、固定サイズの配列に使用する方法でした。

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

29
Furious Coder

特定のサイズのオブジェクトに対して、非常にパフォーマンスの高い特別なヒープアロケータを書くことができます。ただし、generalヒープアロケータは特にパフォーマンスが良くありません。

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

18

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

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

複数のスレッドを持つ32ビットオペレーティングシステムでは、アドレス空間を切り分ける必要があり、遅かれ早かれ1つのスレッドスタックが別のスレッドスタックに達するため、スタックはかなり制限されることがよくあります(通常は少なくとも数MB)。シングルスレッドシステム(とにかくシングルスレッドのLinux glibc)では、スタックが増えるだけなので制限ははるかに少なくなります。

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

7
MarkR

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

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

6

ヒープ割り当てよりも桁違いにパフォーマンスが向上することを除けば、スタック割り当ては長時間実行されるサーバーアプリケーションに適しています。最も管理されたヒープでさえ、最終的には断片化しすぎてアプリケーションのパフォーマンスが低下します。

6
Jay

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

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

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

4
yogman

高速なのはスタック割り当てだけではありません。また、スタック変数を使うことで多くのことを勝ち取ることができます。それらはより良い参照地域を持っています。そして最後に、割り当て解除もはるかに安いです。

3
MSalters

ヒープアロケータが単にスタックベースのアロケーション手法を使用することは確かに可能ですが、スタックアロケーションはほとんどの場合ヒープアロケーションと同じくらい速いか速いです。

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

一方、スレッド数が多すぎると、32ビットメモリ範囲でのスタックベースの割り当ても危険にさらされます。スタックは連続したメモリ範囲を必要とするので、あなたが持っているより多くのスレッド、あなたがそれらがスタックオーバーフローなしで実行するために必要とするより多くの仮想アドレス空間。これは(今のところ)64ビットでは問題になりませんが、多くのスレッドを持つ長期実行プログラムでは確かに大混乱を招く可能性があります。断片化による仮想アドレス空間の不足は、常に対処するのに苦労します。

3
MSN

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

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

3
larsivi

私は、寿命が極めて重要であり、割り当てられるものが複雑な方法で構築されなければならないかどうかを考えます。たとえば、トランザクション駆動型モデリングでは、通常、一連のフィールドを含むトランザクション構造を操作機能に入力して渡す必要があります。例としてOSCI SystemC TLM-​​2.0規格を見てください。

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

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

その理由は、オブジェクトが高価な構造とかなり長い耐用年数を持っているからです。

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

3
jakobengblom2

C++言語に固有の懸念

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

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

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

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

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

C++の実装について

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

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

ほとんどのC++実装(特にISAレベルのネイティブコードをターゲットとし、即時出力としてアセンブリ言語を使用する実装)は、このような類似の戦略を使用するため、このような混乱した「割り当て」スキームが一般的です。このような割り当て(および割り当て解除)はマシンサイクルを消費し、(最適化されていない)呼び出しが頻繁に発生する場合は、最新のCPUマイクロアーキテクチャが一般的なコードパターン(ハードウェアを使用するなど) スタックエンジンPush/POP命令の実装)。

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

メモリアクセスへの影響

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

並行性

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

スペース効率

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

スタック割り当ての制限

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

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

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

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

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

3
FrankHB

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

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

3

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

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

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

あなたがそれがあなたのオブジェクトのヒープ割り当てに多くの時間を費やしているのを見つけた場合にのみ、それらをスタック割り当てすることは著しく速いでしょう。

2
Mike Dunlavey

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

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

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

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

2
wjl
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ではこんな感じでしょう。 funcname__に入っているとき、f1とポインタf2はスタック(自動ストレージ)に割り当てられています。ちなみに、Foo f1(a1)はスタックポインタ(espname __)に命令効果がありません、funcname__がメンバf1を取得したい場合、それは割り当てられています、それはその命令はこのようなものです:lea ecx [ebp+f1], call Foo::SomeFunc()。スタック割り当てによって、メモリがFIFOname__のようなものになっていると思われることもあります。FIFOname__は、関数内に入ってint i = 0のようなものを割り当てた場合には発生しません。

2
bitnick

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

オペレーティングシステムは、空き部分の開始アドレスへのポインタと空き部分のサイズとからなるペイロードデータを有するリンクリストとして空きメモリの部分を維持する。 Xバイトのメモリを割り当てるには、リンクリストをたどり、各ノートを順番に調べて、サイズがX以上であるかどうかを確認します。サイズP> = Xの部分が見つかった場合、Pは次の2つの部分に分割されます。サイズXとPX。リンクリストが更新され、最初の部分へのポインタが返されます。

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

1
Nikhil

一般に、スタック割り当ては、上記のほぼすべての回答で述べられているヒープ割り当てよりも高速です。スタックPushまたはpopはO(1)ですが、ヒープを割り当てたり解放したりするには、以前の割り当てを確認する必要があります。ただし、通常、パフォーマンスが非常に厳しいループで割り当てることは避けてください。そのため、通常は他の要因によって選択されます。

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

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

1
Dan Olson

通常、スタック対ヒープ割り当てを選択する際の考慮事項は速度とパフォーマンスに関するものではありません。スタックはスタックのように動作します。つまり、ブロックをプッシュして再びポップインし、後入れ先出しでポップするのに適しています。プロシージャの実行もスタックのようで、最後に入力したプロシージャが最初に終了します。ほとんどのプログラミング言語では、プロシージャに必要なすべての変数はプロシージャの実行中にのみ表示されるため、プロシージャに入るときにプッシュされ、終了または戻るときにスタックからポップされます。

今度はスタックを使用できない例を示します。

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

プロシージャSでメモリを割り当ててスタックに配置してからSを終了すると、割り当てられたデータはスタックからポップされます。しかし、Pの変数xもそのデータを指していたので、xは今度は未知の内容でスタックポインタの下のどこかを指しています(スタックが下向きに成長すると仮定します)。スタックポインタがその下のデータをクリアせずに上に移動しただけでも内容は残っている可能性がありますが、スタック上で新しいデータの割り当てを開始すると、ポインタxは実際にはその新しいデータを指します。

他のアプリケーションコードや使用方法が機能に影響を与える可能性があるため、時期尚早の仮定をしないでください。そのため、機能を見ても分離は役に立ちません。

あなたがアプリケーションに真剣であるならば、それをVTuneするか、または同様のプロファイリングツールを使用して、ホットスポットを見てください。

ケタン

0
Ketan