web-dev-qa-db-ja.com

動的に割り当てられたアレイの理想的な成長率はどれくらいですか?

C++にはstd :: vectorとJavaにはArrayListがあり、他の多くの言語には独自の形式の動的に割り当てられた配列があります。動的配列がスペースを使い果たすと、より大きな領域に再割り当てされ、古い値が新しい配列にコピーされます。このような配列のパフォーマンスの中心となる問題は、配列のサイズがどれだけ速く成長するかです。常に現在のプッシュに適合するだけの大きさになると、毎回再割り当てされることになります。したがって、配列サイズを2倍にするか、1.5倍にするのが理にかなっています。

理想的な成長要因はありますか? 2倍? 1.5倍?理想的には、数学的に正当化され、パフォーマンスと無駄なメモリのバランスが最適になることを意味します。理論的には、アプリケーションにプッシュの潜在的な分散があり、これがアプリケーションに依存している可能性があることを考えると、しかし、「通常」最良であるか、またはいくつかの厳密な制約内で最良と見なされる値があるかどうかを知りたいと思っています。

これに関する論文がどこかにあると聞きましたが、見つけることができませんでした。

73
Joseph Garvin

それは完全にユースケースに依存します。データをコピーして(そして配列を再割り当てして)無駄に費やした時間や余分なメモリを気にしますか?アレイはどのくらい持続しますか?長期間使用しない場合は、より大きなバッファーを使用することをお勧めします。ペナルティは短期間です。それがぶらぶらするつもりなら(例えば、Javaで、より古い世代やより古い世代になります)、それは明らかにペナルティのより多くです。

「理想的な成長要因」というものはありません。 理論的にアプリケーションに依存するだけでなく、間違いなくアプリケーションに依存します。

2はかなり一般的な成長要因です-ArrayListList<T> in .NETを使用します。 ArrayList<T> in Java 1.5を使用します。

編集:エーリッヒが指摘するように、Dictionary<,> in .NETは「サイズを2倍にしてから次の素数に増やす」を使用するため、ハッシュ値をバケット間で合理的に分散できます。 (私は最近、プライムがハッシュバケットの配布に実際にはそれほど優れていないことを示唆するドキュメントを見たと思いますが、それは別の答えの議論です。)

39
Jon Skeet

私が何年も前に読んだのを覚えています。少なくともC++に適用される場合、1.5が2つよりも好ましい理由です(これはおそらく、ランタイムシステムがオブジェクトを自由に再配置できるマネージ言語には適用されません)。

推論はこれです:

  1. 16バイトの割り当てから始めるとします。
  2. さらに必要な場合は、32バイトを割り当て、16バイトを解放します。これにより、メモリに16バイトの穴が残ります。
  3. さらに必要な場合は、64バイトを割り当てて、32バイトを解放します。これにより、48バイトのホールが残ります(16と32が隣接していた場合)。
  4. さらに必要な場合は、128バイトを割り当て、64バイトを解放します。これにより、112バイトのホールが残ります(以前のすべての割り当てが隣接していると仮定)。
  5. などなど。

アイデアは、2倍の拡張では、結果のホールが次の割り当てに再利用するのに十分な大きさになることはないということです。 1.5xの割り当てを使用すると、代わりに次のようになります。

  1. 16バイトで開始します。
  2. さらに必要な場合は、24バイトを割り当て、16バイトを解放して、16バイトのホールを残します。
  3. さらに必要な場合は、36バイトを割り当て、24バイトを解放して、40バイトのホールを残します。
  4. さらに必要な場合は、54バイトを割り当ててから36バイトを解放し、76バイトのホールを残してください。
  5. さらに必要な場合は、81バイトを割り当ててから54バイトを解放し、130バイトのホールを残します。
  6. さらに必要な場合は、130バイトの穴から122バイト(切り上げ)を使用します。
88

理想的には(n→∞の範囲で) それは黄金比です :ϕ = 1.618 ...

実際には、1.5のような近いものが必要です。

その理由は、古いメモリブロックを再利用して、キャッシュを利用し、OSが常により多くのメモリページを提供することを避けたいためです。これがxに減少することを確認するために解く方程式n− 1 − 1 =xn+ 1xn、その解はx= largenに近づきます。

38
Mehrdad

このような質問に答えるときの1つのアプローチは、広く使用されているライブラリーが少なくともひどいことをしていないという前提の下で、単に「チート」して人気のあるライブラリーが何をしているかを調べることです。

したがって、非常に迅速にチェックするだけで、Ruby(1.9.1-p129)は配列に追加するときに1.5xを使用するように見え、Python(2.6.2) 1.125xと定数を使用します( Objects/listobject.c 内):

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
    PyErr_NoMemory();
    return -1;
} else {
    new_allocated += newsize;
}

上記のnewsizeは、配列の要素数です。 newsizenew_allocatedに追加されているため、ビットシフトと3項演算子を含む式は実際には割り当て超過を計算しているだけです。

11
Jason Creighton

配列サイズをxだけ大きくするとします。したがって、サイズTから始めると想定します。次に配列を拡大したときのサイズは_T*x_になります。次に、_T*x^2_などになります。

以前に作成したメモリを再利用できるようにすることが目標である場合は、割り当てる新しいメモリが、割り当てを解除した以前のメモリの合計よりも少ないことを確認する必要があります。したがって、次の不等式があります。

_T*x^n <= T + T*x + T*x^2 + ... + T*x^(n-2)
_

Tを両側から削除できます。したがって、これを取得します。

_x^n <= 1 + x + x^2 + ... + x^(n-2)
_

非公式に言うと、nth割り当てでは、以前に割り当てを解除したすべてのメモリをn番目の割り当てで必要なメモリ以上にして、以前に割り当てを解除したメモリを再利用できるようにします。

たとえば、これを3番目のステップで実行したい場合(つまり、_n=3_)、次のようになります。

_x^3 <= 1 + x 
_

この方程式は、_0 < x <= 1.3_(ほぼ)であるすべてのxに当てはまります。

以下のさまざまなnに対して何が得られるかを確認します。

_n  maximum-x (roughly)

3  1.3

4  1.4

5  1.53

6  1.57

7  1.59

22 1.61
_

x^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2から、増加係数は_2_より小さくなければならないことに注意してください。

7
CEGRD

それは本当に依存します。一部の人々は、最適な数を見つけるために一般的な使用事例を分析します。

1.5x 2.0x phi xと2の累乗が以前に使用されたのを見ました。

4
Unknown

配列の長さの分布があり、スペースの浪費と時間の浪費のどちらを好むかを示すユーティリティ関数がある場合は、最適なサイズ変更(および初期サイズ変更)戦略を確実に選択できます。

単純な定数倍数が使用される理由は、各追加が定数時間を償却するためです。ただし、これは、小さいサイズに対して異なる(大きい)比率を使用できないことを意味するものではありません。

Scalaでは、標準ライブラリハッシュテーブルのloadFactorを、現在のサイズを調べる関数でオーバーライドできます。奇妙なことに、サイズ変更可能な配列は2倍になるだけです。これは、ほとんどの人が実際に行うことです。

実際にメモリエラーをキャッチし、その場合の成長が少なくなる2倍(または1.5倍)の配列については知りません。あなたが巨大な単一の配列を持っているなら、あなたはそれをしたいと思うようです。

サイズ変更可能な配列を十分に長く維持していて、時間の経過とともにスペースを優先する場合は、最初に(ほとんどの場合)劇的に全体を割り当て、次に適切なサイズに正確に再割り当てすることは理にかなっている可能性があります。完了しました。

2
Jonathan Graehl

私はジョン・スキートに同意します。私の理論製作者の友人でさえも、これをO(1)にすると、係数を2倍に設定すると証明できると主張します。

CPU時間とメモリの比率はマシンごとに異なるため、係数も同様に異なります。ギガバイトのRAMが搭載されたマシンでCPUが遅い場合、要素を新しいアレイにコピーすると、高速のマシンよりもはるかにコストがかかり、メモリが少なくなる可能性があります。これは、理論的には答えられる質問です。統一されたコンピューターの場合、実際のシナリオではまったく役に立ちません。

1
Tom

さらに2セント

  • ほとんどのコンピュータには仮想メモリがあります!物理メモリでは、プログラムの仮想メモリ内の単一の連続した領域として表示されるランダムなページを随所に配置できます。間接の解決はハードウェアによって行われます。 32ビットシステムでは、仮想メモリの枯渇が問題でしたが、実際にはもう問題ではありません。したがって、を埋めることはもう問題ではありません(特別な環境を除く)。 Windows 7以降、Microsoftも64ビットをサポートしています。 @ 2011
  • O(1)は、任意のr> 1因子で到達します。同じ数学的証明は、パラメーターとして2だけでは機能しません。
  • r = 1.5はold*3/2で計算できるため、浮動小数点演算は必要ありません。 (コンパイラーは、生成されたアセンブリコードのビットシフトに適合している場合、コンパイラーがそれをビットシフトに置き換えるため、/2と言います。)
  • MSVCはr = 1.5を採用しているため、比率として2を使用しない主要なコンパイラが少なくとも1つあります。

誰かが述べたように、2は8よりも気持ちがいい。また、2は1.1よりも気持ちがいい。

私の考えでは、1.5が適切なデフォルトです。それ以外は特定のケースに依存します。

0
Notinlist

私はそれが古い質問であることを知っていますが、皆が見逃しているように見えるいくつかの事柄があります。

まず、これは2による乗算です。サイズ<<1。これは、1から2までのanythingによる乗算です。int(float(size)* x)、ここでxは数値、*は浮動小数点ですポイント数学、そしてプロセッサはfloatとintの間でキャストするための追加の命令を実行する必要があります。つまり、マシンレベルでは、ダブリングは新しいサイズを見つけるために単一の非常に高速な命令を必要とします。 1と2の間の乗算には、サイズをfloatにキャストするための1つの命令が必要です少なくとも乗算するための1つの命令(これはfloatの乗算なので、4でない場合でも、少なくとも2倍のサイクルが必要です)または8倍)、およびintにキャストバックするための1つの命令であり、特殊レジスターの使用を要求する代わりに、プラットフォームが汎用レジスターで浮動小数点演算を実行できると想定しています。つまり、各割り当ての計算には、単純な左シフトの少なくとも10倍の時間がかかることを期待する必要があります。ただし、再割り当て中に大量のデータをコピーする場合、これは大きな違いにはなりません。

第二に、おそらく大きなキッカー:誰もが解放されているメモリは、それ自体に隣接しているだけでなく、新しく割り当てられたメモリにも隣接していると想定しているようです。すべてのメモリを自分で事前に割り当ててから、それをプールとして使用しない限り、これはほぼ確実に当てはまりません。 OS たまにある可能性がありますこれで終わりますが、ほとんどの場合、十分な空き領域の断片化があり、メモリ管理システムがメモリの小さな穴を見つけることができますジャストフィット。本当にビットチャンクになると、連続した断片になる可能性が高くなりますが、そのときまでに、割り当ては十分に大きいため、問題が発生するほど頻繁に実行されていません。要するに、理想的な数を使用すると空きメモリ領域を最も効率的に使用できると想像するのは楽しいですが、実際には、プログラムがベアメタルで実行されていない限り(OSがないなど)、それは起こりません。その下ですべての決定を行います)。

質問に対する私の答えは?いいえ、理想的な数はありません。これはアプリケーション固有なので、実際には誰も試しません。あなたの目標が理想的なメモリ使用量である場合、あなたはかなり運が悪いです。パフォーマンスについては、割り当ての頻度を少なくすることをお勧めしますが、それだけを行うと、4または8を掛けることができます。もちろん、Firefoxが1 GBから8 GBに一気に使用すると、不満が出てくるので、それでも意味がありません。ここに私が通るだろういくつかの経験則があります:

メモリ使用量を最適化できない場合は、少なくともプロセッササイクルを無駄にしないでください。 2を掛けると、浮動小数点演算よりも少なくとも1桁速くなります。大きな違いはないかもしれませんが、少なくともある程度は違います(特に早い段階で、より頻繁で小さい割り当ての場合)。

考えすぎないでください。すでに行われていることを実行する方法を理解するために4時間を費やしただけの場合は、時間を無駄にしているだけです。正直なところ、* 2よりも優れたオプションがあった場合、それは数十年前にC++ベクトルクラス(および他の多くの場所)で行われたはずです。

最後に、もしあなたが本当に最適化したいなら、小さなことを気にしないでください。今日、組み込みシステムで作業しているのでない限り、4KBのメモリが無駄になることを気にする人はいません。それぞれ1MBから10MBの間の1GBのオブジェクトに到達すると、倍加は多すぎる(つまり、100から1,000オブジェクトの間の)ことになります。予想される拡張率を見積もることができる場合は、特定の時点でそれを線形成長率に揃えることができます。 1分あたり約10個のオブジェクトが予想される場合、1ステップあたり5〜10個のオブジェクトサイズで(30秒から1分ごとに1回)成長することはおそらく問題ありません。

それがすべての結果となるのは、それを考えすぎないで、できることを最適化し、必要に応じてアプリケーション(およびプラットフォーム)にカスタマイズすることです。

0
Rybec Arethdar