web-dev-qa-db-ja.com

C ++では;ヒープへの委任を検討する前に、[関数間で転送される]オブジェクトはどのくらいの大きさにする必要がありますか?

私の日常のプログラミングでは、コードを単純でエラーのない状態に保ちたいだけでなく、委任から利益を得るのに十分な大きさのオブジェクトがプログラミングにないことを想定しているため、ポインタをほとんど使用しません。それらをヒープに。

せいぜい;私の最大のオブジェクトは、おそらく100万文字のQStringでしょう。

ただし、ヒープの恩恵を受けるには十分な大きさではないと思います。作成したオブジェクトがポインタに変換することでメリットを得るのに十分な大きさであるかどうかを推定するにはどうすればよいですか?

7
Akiva

ほとんどの実装は、ヒープとスタックを同じメモリブロックから取得するため(どちらの端からでも増加します)、問題ではありません。サイズは、スタックよりもヒープを優先する理由ではありません。オブジェクトの寿命です。範囲外になった場合に死ぬべきか?そうでない場合、いつですか?

12
candied_orange

値渡し(レジスターに保持できる2ワード以上)はスタックを介して行われるため、100万文字は値渡しされません。また、スタックスペースは常に制限されています。

さいわい、QStringを使用すると、小さなオブジェクトだけが値で渡されます。オブジェクト自体は、数百万バイトが格納されているメモリ領域へのポインタを使用します。それでも、値渡しとは、数百万バイトをコピーする(移動のセマンティクスを使用できる場合を除いて)コピーが作成されることを意味します。値渡しが必要ない場合は、const参照を渡すことをお勧めします。

最後に、unique_ptrやshared_ptrなどの標準のスマートポインターを使用して、安全な方法でポインターを使用することを検討できます。

8
Christophe

したがって、基本的には「いつnewを使用すべきですか?」ローカル変数は常にスタックスペースを使用しますが、new式を使用してヒープ上のオブジェクトを明示的に割り当てることができます。

newに賛成するか反対するかを決定するとき、より重要な基準があるため、オブジェクトのサイズに基づいて決定することはありません。

  • オブジェクトのサイズを静的に知っていますか?スタックに配列を割り当てることはあまり意味がありません。通常、配列は静的または動的に割り当てられます。

  • オブジェクトの寿命はどれくらいですか?制御フローが現在のスコープを離れると、その寿命は終了しますか?その場合、ローカル変数を使用してオブジェクトを作成する必要があります。それがより長く存続できるはずであれば、ヒープに割り当てられたオブジェクトが必要です。

  • 例外セーフなコードを書こうとしているのでしょうか? (はい!)その場合、複数のネイキッドポインターの所有権を処理するのは非常に面倒で、バグが発生しやすくなります。 newを使用してオブジェクトを割り当てる場合でも、_std::unique_ptr_のようなスマートポインターを介してオブジェクトを管理する必要があります。

ほとんどのオブジェクトはかなり小さいです。例えば。 _std::vector_ローカル変数は、格納するデータの量に関係なく、おそらく3ワードのスタックメモリのみを使用します。内部的には、ヒープ上のデータにバッファを割り当てます。同じことが文字列型にも当てはまります。かなり大きなクラスを作成する場合、サイズを小さくする方法があります。しかし、それが発生するずっと前に、インスタンスフィールドの数が増えると、単一責任の原則が思い出されるはずです。複数の小さなオブジェクトのコラボレーションとして、クラスをより簡単に説明できますか?しかし、それは設計上の問題です。

オブジェクトをパラメーターとして関数に渡す場合、呼び出し元は変数を渡す方法を決定できません。これは関数シグネチャの一部です。オブジェクトがスタックに割り当てられている場合でも、ポインターまたは参照によってオブジェクトを渡すことができることに注意してください。可能な引数のタイプは次のとおりです。

  • Const参照_const T& arg_は、パラメーターを渡す通常の方法です。参照はポインタに似ているため、T型のサイズは無関係です。

  • コピー_T arg_は通常、次の2つのケースのいずれかでのみ実行されます。

    1. サイズは非常に小さく(数語まで)、タイプは安価にコピーでき、仮想メソッドはありません。これは、組み込みの数値型と、おそらく小さなユーザー定義のPOCO(構造体などのプレーンな古いCオブジェクト)について説明しています。
    2. とにかく、コピーを作成する必要があります。これは、T operator+(T lhs, T const& rhs) { lhs += rhs; return lhs; }のような演算子のオーバーロードで時々見られます。
  • 非const参照_T& arg_は、オブジェクトを変更する必要がある場合、または型Tがconstの正確さを考慮して記述されていない場合に使用されます。

  • ポインター_const T* arg_または_T* arg_は、最近のC++コードではめったに見られません。参照は、既存のオブジェクトからのみ作成できます。対照的に、ポインタはnullになるか、無効なメモリをポイントします。そのため、コピーを回避する(参照を使用する)ため、または仮想メソッド呼び出しを可能にする(ここでも参照を使用する)ために、ポインターを使用しないでください。これは、ポインタのようなものへのポインタを残します:配列としてポインタを使用する(標準のコレクションを優先する)、ポインタ計算を行う(イテレータを優先する)、または別のオブジェクトを指すようにポインタを再割り当てする場合、参照では不可能です。

6
amon

せいぜい;私の最大のオブジェクトはおそらく100万文字のQStringでしょう。

_std::vector_の場合と同様に、コンテンツは常にヒープに割り当てられるため、sizeof(QString)は64ビットシステムでは8バイトです。 QStringは、事実上、ヒープ上のメモリへの単なるハンドルです。したがって、スタックにQStringオブジェクトを作成し、それに100万文字を挿入しても、レジスターに直接存在しない場合でも、最大で8バイトのスタック領域を使用します。

スタックに100万のUnicode文字を割り当てようとした場合、多くの場合、オーバーフローを要求することになります。

人々が書く通常の種類のC++コードのほとんどでは、スタックとヒープの違いについては、使用する標準のデータ構造ですべて抽象化されているため、一般的にそれほど考慮する必要はありません。一般的には、クラスを実装する際に、たとえば_unique_ptr_を使用するかどうかを区別します。もしそうなら、それは追加のヒープオーバーヘッド、間接の追加の層、そしていくらかのメモリの断片化を意味します。

スタックで_std::array<T, N>_を使用したいだけのように、ヒープ上で独自のメモリを個別に管理しないオブジェクトを操作する場合、適切な範囲(大まかな粗雑な数値) )は、通常、再帰的に呼び出されない関数や、ほとんどのデスクトップマシンとオペレーティングシステムでスタックオーバーフローに対して合理的に安全になるように大量のスタックスペースを割り当てる他の関数を呼び出す関数の場合、数百バイトから数キロバイトの範囲です。少なくともシステム。 32ビット整数の配列の場合、128(128バイト以上)を格納する場合はヒープを使用する可能性があります。たとえば、代わりに_std::vector_を使用できます。例:

_void some_func(int n, ...)
{
    // 'std::array' is a purely contiguous structure (not
    // storing pointers to memory allocated elsewhere), so
    // here we are actually allocating sizeof(int) * 128
    // bytes on the stack.
    std::array<int, 128> stack_array;
    int* data = stack_array.data();

    // Only use heap if n is too large to fit in our
    // stack-allocated array of 128 integers.
    std::vector<int> heap_array;
    if (n > stack.size())
    {
        heap_array.resize(n);
        data = heap_array.data();
    }

    // Do stuff with 'data'.
    ...      
}
_

移動のセマンティクスについては、スタックからスタックにメモリをコピーする場合、または時間的局所性の高いメモリの2つの領域からメモリをコピーする場合は、512バイトでもディープコピーする方が実際には非常に安価であるため、同様のルールを適用します。巨大なクラスFooがあり、12個のデータメンバーがあり、sizeof(Foo)がなんと256バイトのようなものである場合でも、ヒープにコンテンツを割り当てるためにそれほど多くは使用しません。回避できる場合。スタックオーバーフローの回避や可変サイズのデータ​​構造のモデリングなど、パフォーマンス以外の事柄に基づいてヒープを使用するかどうかを決定する場合、通常は完全に問題ありません(小さなサイズを含む一般的なケースに最適化されていない限りヒープを意味します)。 、小さな文字列の余分なヒープ割り当てを回避し、大きな文字列には追加のヒープ割り当てを使用する小さな文字列最適化のように、_shared_ptr_で共有所有権を許可し、pimplを使用してコンパイル時の依存関係を減らすか、またはポリモーフィズムにサブタイプを割り当てるこれらの場合にヒープ割り当てを回避するのは扱いにくいかもしれません。

2
user204677

ほとんどの場合、キャンディーオレンジの答えは正しいです。一般に、関数を終了するときに何かを消したい場合は、スタックに入れます。ただし、注意が必要な微妙な点が1つあります。スタックにはサイズがあります。マルチスレッドアプリケーションを使用している場合は、複数のスタック(スレッドごとに1つ)があります。これらのスタックはしばしば隣接しています。つまり、プロセスに割り当てられたメモリブロック内で、1つのスタックが次のスタックの直後に来ます。 1つのスタックに情報を入れすぎると、オーバーフローして次のスタックにヒットし、破損したり、保護されたページに上書きしてクラッシュする可能性があります。

これは、小さなオブジェクトで構成される大きなオブジェクト、および自分自身を呼び出してローカル変数の複数のコピーをスタックに置く再帰関数の場合に発生する可能性が高くなります。再帰関数のスタック(または、相互に呼び出す非再帰関数のスタック)に特に大きなオブジェクトがある場合、問題が発生する可能性があります。したがって、スタックに配置するオブジェクトのサイズに注意することが重要です。スタックの深さが大きくなり、スタックで操作しているオブジェクトのサイズが大きくなると、問題が発生するリスクがあります。

いくつかの解決策があります。 QStringの処理を実行し、オブジェクトにヒープ上のメモリを割り当てて、スタックに1を置いてもスタックスペースが少しだけ使用され、残りはヒープ上にあるようにすることができます。他の人が述べたように、ヒープから割り当ててスタック上のスマートポインタを使用することができます。または、この問題のない小さなオブジェクトを探すこともできます。それはすべてあなたが解決している問題に依存します。

ただし、オブジェクトのサイズによって、割り当て先や処理方法が決まる場合があることに注意してください。

1
user1118321

場合によります。

何よりもまず、最も重要なのはセマンティクスです。値で返すことは、参照型には意味がありません。

また、移動セマンティクスの出現により、値による受け渡しはコピーを作成することを意味しないため、「大きな」オブジェクトの場合でも効率的な場合があります。そして、RVOを忘れないようにしましょう。

そうは言っても、特にオブジェクト自体に内部ヒープ割り当てがある場合や、タイトなループで実行される場合は特に、小さいオブジェクトでも値による受け渡しは高くつく可能性があります。

基本的に、プロフィルが問題であることがわかるまで、値渡しを続けることができます。これの私のヒューリスティックは、特にそれがタイトなループでない限り、ヒープの割り当て/同期が必要ない場合、100バイト未満のものについて心配することではありません。

1
Bwmat