質問

この質問はかなり初歩的なものに聞こえるかもしれませんが、これは私が一緒に仕事をしている別の開発者と交わした議論です。

私は、ヒープ割り当てではなく、できる限りスタック割り当てするように気をつけていました。彼は私に話しかけたり肩越しに見守ったりしていましたが、性能的には同じなのでその必要はないとコメントしました。

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

これはおそらくコンパイラに大きく依存するものだと思います。特にこのプロジェクトでは、 メトロワークス のコンパイラ PPC 建築。この組み合わせに関する洞察は非常に役立ちますが、一般に、GCC と MSVC++ の場合はどうなるでしょうか?ヒープ割り当てはスタック割り当てほどパフォーマンスが高くありませんか?違いはありませんか?それとも、違いが非常に小さいため、無意味なマイクロ最適化になってしまうのでしょうか。

役に立ちましたか?

解決

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

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

他のヒント

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

sub esp, 0x10

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

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

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

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

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

#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";
}

愚かな一貫性は小さな心のホブゴブリンです。どうやら最適化コンパイラは、多くのプログラマーの心のホブゴブリンです。この議論はかつては答えの一番下にありましたが、人々はそこまで読むことを気にすることはないようですので、すでに答えた質問が出ないようにここに移動します。

最適化コンパイラは、このコードが何もしないことに気付き、すべてを最適化する可能性があります。そのようなことをするのはオプティマイザーの仕事であり、オプティマイザーと戦うのはばかげたことです。

現在使用中の、または将来使用されるすべてのオプティマイザをだます良い方法がないため、最適化をオフにしてこのコードをコンパイルすることをお勧めします。

オプティマイザーをオンにして、それとの戦いについて文句を言う人は誰でもpublic笑されるべきです。

ナノ秒の精度を気にする場合、 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を使用しているマシンでは、「0クロックティック」が発生します。スタックとヒープの両方の割り当てが100000未満の割り当ての場合、それでも「0クロックティック」が発生します。スタック割り当てと「15クロックティック」用ヒープ割り当て用。 10,000,000の割り当てを測定すると、スタックの割り当てには31クロックticかかります

Xbox 360キセノンプロセッサのスタックとヒープの割り当てについて学んだ興味深いことは、他のマルチコアシステムにも適用される可能性がありますが、ヒープに割り当てるとクリティカルセクションに入り、他のすべてのコアが停止することですallocは競合しません。したがって、タイトループでは、スタック割り当ては、ストールを防止するために固定サイズのアレイを使用する方法でした。

これは、マルチコア/マルチプロシージャ用にコーディングしている場合に考慮すべき別の高速化である可能性があります。スタック割り当ては、スコープ関数を実行しているコアによってのみ表示され、他のコア/ CPUには影響しません。

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

また、オブジェクトの予想される寿命について、Torbj&#246; rn Gyllebringに同意します。良い点!

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

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

複数のスレッドを持つ32ビットオペレーティングシステムでは、アドレススペースを切り分ける必要があり、遅かれ早かれ1つのスレッドスタックが別のスレッドスタックに実行されるため、スタックは多くの場合かなり制限されます(通常は少なくとも数mbまで) 。シングルスレッドシステム(とにかくLinux glibcシングルスレッド)では、スタックが成長し、成長するだけなので、制限ははるかに小さくなります。

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

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

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

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

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

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

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

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

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

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

その理由は単に、オブジェクトの構造が高価であり、耐用年数がかなり長いためです。

私ならこう言います:実際にはコードの動作に依存する可能性があるため、両方を試して、自分のケースに最適な方法を確認してください。

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

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

スタックの割り当てだけでなく、高速です。また、スタック変数を使用することで多くの勝利を収めます。参照の局所性が向上しています。そして最後に、割り当て解除もずっと安くなります。

スタックの割り当ては、ヒープの割り当てよりもほぼ常に高速または高速になりますが、ヒープアロケータが単にスタックベースの割り当て手法を使用することは確かに可能です。

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

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

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

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

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

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

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

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

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

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

たとえば、スタックに何かを割り当ててからコンテナに入れる場合は、ヒープに割り当ててポインタをコンテナに保存する方がよいでしょう(たとえば、std :: shared_ptr&lt;&gt; )。値によってオブジェクトを渡したり返したりする場合、および他の同様のシナリオでも同じことが当てはまります。

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

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ではこのようになります。 func では、 f1 とポインター f2 がスタックに割り当てられています(自動ストレージ)。ちなみに、Foo f1(a1)はスタックポインター( esp )に命令効果を持たず、 func が必要な場合は割り当てられています。メンバー f1 を取得すると、命令は次のようになります。 lea ecx [ebp + f1]、Foo :: SomeFunc()を呼び出します。スタックが割り当てる別のことは、誰かがメモリを FIFO のようなものだと思うようにするかもしれません、あなたが関数にいて何かを割り当てる場合、 FIFO はある関数に入ったときにちょうど起こりました int i = 0 のように、プッシュは発生しませんでした。

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

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

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

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

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

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

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

スタックを使用できない例の場合:

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

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

C ++言語に固有の懸念

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

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

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

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

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

C ++の実装について

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

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

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

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

メモリアクセスへの影響

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

同時実行性

メモリの同時アクセスの期待は、スタックとヒープ間で異なる影響を与える可能性があります。通常、コールスタックは、C ++実装の1つの実行スレッドによって排他的に所有されます。 OTOH、ヒープは多くの場合<

他のアプリケーションコードや使用法が機能に影響を与える可能性があるため、時期尚早な仮定を行わないでください。そのため、関数を見るのは分離では意味がありません。

アプリケーションを真剣に考えている場合は、VTuneを使用するか、同様のプロファイリングツールを使用してホットスポットを確認してください。

ケタン

実際に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だけが別の値に変わります。ローカル変数がなかった場合を除き、この命令を実行するだけです。したがって、ローカル変数の割り当てにはオーバーヘッドがありません。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top