C ++ベクトルのpush_backが常に償却されるのはなぜですか?


23

私はC ++を学習していますが、ベクターのpush_back関数の実行時間が一定の「償却」であることに気付きました。文書にはさらに、「再割り当てが行われた場合、再割り当て自体が全体のサイズで線形になります」と記載されています。

これは、push_back関数がであることを意味するべきではありません。ここで、nはベクトルの長さです。結局のところ、最悪の場合の分析に興味がありますよね?O(n)n

決定的に、形容詞「償却」が実行時間をどのように変えるか理解していないと思います。


RAMマシンでは、バイトのメモリを割り当てることはO n 操作ではありません。ほぼ一定の時間と見なされます。nO(n)
-usul

回答:


24

ここで重要な言葉は「償却」です。償却分析は、操作のシーケンスを調べる分析手法です。シーケンス全体がT n 時間で実行される場合、シーケンスの各操作はT n / nで実行されます。アイデアは、シーケンス内のいくつかの操作はコストがかかるかもしれませんが、プログラムを圧迫するほど頻繁に実行できないことです。これは、一部の入力分布またはランダム化分析での平均ケース分析とは異なることに注意することが重要です。償却分析は最悪のケースを確立しましたnT(n)T(n)/n入力に関係なく、アルゴリズムのパフォーマンスの限界。これは、プログラム全体で永続的な状態を持つデータ構造の分析に最もよく使用されます。

kO(n)nO(n)O(1)

m

nmlogm(n)imini=1logm(n)minmm1nmm1m1m1.5


12

@Marcは(私が思うに)優れた分析を提供しましたが、一部の人々は、わずかに異なる角度から物事を検討することを好むかもしれません。

1つは、再割り当てを行うわずかに異なる方法を検討することです。古いストレージから新しいストレージにすべての要素をすぐにコピーする代わりに、一度に1つの要素のみをコピーすることを検討してください。つまり、push_backを実行するたびに、新しい要素を新しいスペースに追加し、既存の要素を1つだけコピーします古いスペースから新しいスペースへの要素。成長因子を2と仮定すると、新しいスペースがいっぱいになると、古いスペースから新しいスペースへのすべての要素のコピーが終了し、各push_backは正確に一定の時間になったことは明らかです。その時点で、古いスペースを破棄し、2倍のゲインであった新しいメモリブロックを割り当てて、プロセスを繰り返します。

かなり明確に、これを無期限に続行できます(または、使用可能なメモリがある限り)。すべてのpush_backには、1つの新しい要素の追加と1つの古い要素のコピーが含まれます。

通常の実装では、コピーの数はまったく同じですが、一度に1つずつコピーするのではなく、既存のすべての要素を一度にコピーします。一方で、あなたは正しいです。つまり、push_backの個々の呼び出しを見ると、それらのいくつかは他のものよりもかなり遅くなることを意味します。ただし、長期平均を見ると、push_backの呼び出しごとに行われるコピーの量は、ベクトルのサイズに関係なく一定のままです。

計算の複雑さには関係ありませんが、push_backごとに1つの要素をコピーするのではなく、push_backごとの時間を一定に保つのではなく、なぜそうするのが有利なのかを指摘する価値があると思います。考慮すべき理由は少なくとも3つあります。

1つ目は、メモリの可用性です。古いメモリは、コピーが終了した後にのみ、他の用途のために解放できます。一度に1つの項目のみをコピーした場合、古いメモリブロックはずっと長く割り当てられたままになります。実際、本質的に常に1つの古いブロックと1つの新しいブロックが割り当てられます。2より小さい成長係数(通常は必要)を決定した場合、常により多くのメモリを割り当てる必要があります。

第二に、一度に1つの古い要素のみをコピーした場合、配列へのインデックス付けは少し難しくなります。各インデックス付け操作では、指定されたインデックスの要素が現在メモリの古いブロックにあるか、新しいもの。これは決して複雑なことではありませんが、配列へのインデックス付けなどの基本的な操作では、ほとんどすべてのスローダウンが重要になります。

第三に、一度にすべてをコピーすることにより、キャッシュをはるかに活用できます。すべてを一度にコピーすると、ほとんどの場合、コピー元とコピー先の両方がキャッシュにあることが予想されるため、キャッシュミスのコストはキャッシュラインに収まる要素の数に応じて償却されます。一度に1つの要素をコピーすると、コピーするすべての要素のキャッシュミスが簡単に発生する可能性があります。これは、複雑さではなく、一定の要因のみを変更しますが、それでもかなり重要になる可能性があります。典型的なマシンでは、10〜20の要因を簡単に予想できます。

一時的に他の方向を検討することもおそらく価値があります。リアルタイム要件を備えたシステムを設計している場合、一度にすべての要素をコピーするのではなく、一度に1つの要素のみをコピーするのが理にかなっています。全体的な速度は遅くなるかもしれませんが、そうではないかもしれませんが、push_backの1回の実行にかかる時間には厳しい上限があります-リアルタイムアロケーターがあると仮定すると(もちろん、多くのリアルタイムシステムは、少なくともリアルタイム要件のある部分では、メモリの動的割り当てをまったく禁止します)。


2
+1これは素晴らしいファインマンスタイルの説明です。
モニカの
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.