534

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

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

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

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

4

23 に答える 23

524

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

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

于 2008-10-02T06:09:03.710 に答える
170

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

sub esp, 0x10

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

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

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

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

于 2008-10-02T06:16:44.673 に答える
122

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

#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正しくコンパイルされないことがよくあります (PDF)。volatile

  3. ループ内のアドレスを取得します (別のファイルで宣言および定義され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
于 2008-10-02T18:11:57.543 に答える
31

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

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

于 2009-03-02T01:55:50.693 に答える
19

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

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

于 2008-10-02T06:08:22.463 に答える
10

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

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

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

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

割り当て関数は、メモリのリソースを割り当てます。アロケータを使用した割り当てに基づいて、オブジェクトをさらに割り当てることができます。自動オブジェクトの場合、それらは直接提示されます - ただし、基になるメモリにアクセスして、他のオブジェクトにメモリを提供するために使用できますが (配置によって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 が物理メモリのスワップとして使用されている場合に特にひどいものです。これにより、非常に長いレイテンシが発生する可能性があります - 時には数十億サイクルです。

スタック割り当ての制限

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

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

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

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

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

于 2018-11-03T15:21:56.393 に答える
9

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

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

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

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

于 2008-10-02T06:12:26.273 に答える
7

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

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

于 2008-10-02T06:18:26.713 に答える
6

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

于 2009-10-26T17:36:00.373 に答える
4

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

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

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

于 2008-10-02T16:57:39.033 に答える
4

より高速なのはjsutスタック割り当てではありません。また、スタック変数を使用すると、多くのメリットがあります。それらは参照のより良い局所性を持っています。そして最後に、割り当て解除も非常に安価です。

于 2008-10-03T15:35:41.137 に答える
4

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

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

于 2008-10-02T08:34:12.410 に答える
3

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

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

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

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

于 2011-06-05T15:46:32.107 に答える
3

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

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

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

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

コードの動作に大きく依存する可能性があるため、両方を試して、自分のケースで何が最適かを確認してください。

于 2008-10-02T06:43:14.390 に答える
3

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

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

于 2010-08-17T20:22:42.990 に答える
2

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

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

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

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

于 2013-06-07T08:24:38.797 に答える
2

このような最適化については、一般的なポイントがあります。

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

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

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

于 2009-01-27T20:41:51.987 に答える
2

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

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

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

于 2010-08-10T16:27:39.993 に答える
1

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

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

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

于 2009-03-02T00:46:06.900 に答える
1

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

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

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

于 2009-03-02T01:36:43.877 に答える
0

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

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

ケタン

于 2009-02-04T17:21:37.590 に答える
-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 またはスイッチ内であっても)、3880 だけが別の値に変更されます。ローカル変数がない場合を除き、この命令を実行するだけです。したがって、ローカル変数の割り当てにはオーバーヘッドがありません。

于 2013-07-24T07:04:22.440 に答える