C ++でベクターを介してリストを使用する意味は何ですか?


32

C ++リストとベクターを含む3つの異なる実験を実行しました。

中間に多くの挿入が含まれていた場合でも、ベクトルを使用したものはより効率的であることが証明されました。

したがって、質問:どの場合、リストはベクトルよりも意味がありますか?

ほとんどの場合、ベクトルがより効率的であると思われ、それらのメンバーがどれだけ似ているかを考えると、リストにはどの利点が残っていますか?

  1. N個の整数を生成し、コンテナに入れて、コンテナがソートされたままになるようにします。挿入は、要素を1つずつ読み取り、最初の大きい要素の直前に新しい要素を挿入することにより、単純に実行されました。
    リストを使用すると、ベクトルと比較して寸法が大きくなると屋根を通過します。

  2. コンテナの最後にN個の整数を挿入します。
    リストとベクトルの場合、時間は同じ桁数増加しましたが、ベクトルの場合は3倍高速でした。

  3. コンテナにN個の整数を挿入します。
    タイマーを開始します。
    リストにはlist.sortを使用し、ベクターにはstd :: sortを使用してコンテナをソートします。タイマーを停止します。
    繰り返しますが、時間は同じ桁で増加しますが、ベクトルでは平均で5倍速くなります。

引き続きテストを実行し、リストの方が良い例がいくつか見つかるかもしれません。

しかし、このメッセージを読んでいる皆さんの共同経験は、より生産的な答えを提供するかもしれません。

リストの使用がより便利だったり、パフォーマンスが向上したりする状況に出くわしたことがありますか?



1
このテーマに関する別の優れたリソースもあります:stackoverflow.com/a/2209564/8360、私が聞いたC ++ガイダンスのほとんどは、デフォルトでvectorを使用することであり、特定の理由がある場合にのみリストします。
ザカリーイェーツ

ありがとうございました。しかし、私は好きな答えで言われていることのほとんどに同意しません。これらの先入観のほとんどは、私の実験によって無効にされました。この人はテストを行っておらず、書籍や学校で教えられている広範な理論を適用していません。
マレックスタンレー

1
Aは、listあなたが要素の多くを削除する場合、おそらくより良い行います。vectorベクター全体が削除されるまで、システムにメモリが返されるとは思わない。また、テスト1では挿入時間だけをテストしているわけではないことに注意してください。これは、検索と挿入を組み合わせたテストです。挿入listが遅い場所を見つけることです。実際の挿入はベクターよりも高速です。
ロボットをゲット

3
非常に一般的であるため、この質問は(実行時)パフォーマンス、パフォーマンス、およびパフォーマンスのみの観点から説明されます。これは、多くのプログラマーの盲点のようです-彼らはこの側面に焦点を当て、多くの場合、はるかに重要な他の側面がたくさんあることを忘れています。
Doc Brown

回答:


34

簡単な答えは、ケースはごくわずかであるように見えるということです。おそらくいくつかあります。

1つは、少数の大きなオブジェクトを保存する必要がある場合です。特に、数個の余分なオブジェクトにスペースを割り当てるのが実用的ではないほど大きいオブジェクトです。基本的に、ベクターまたはdequeが余分なオブジェクトにスペースを割り当てるのを止める方法はありません-それがどのように定義されるかです(つまり、複雑さの要件を満たすために余分なスペースを割り当てる必要があります)。フラットアウトで余分なスペースを割り当てることができないstd::list場合、ニーズを満たす唯一の標準コンテナがあります。

別の方法は、イテレータをリストの「興味深い」ポイントに長期間保存する場合/挿入する場合、および挿入や削除を行う場合、(ほぼ)常にどこからでもそれを行うことです。既にイテレータを持っているので、挿入や削除を行うポイントに到達するためにリストを歩く必要はありません。複数のスポットで作業する場合にも同じことが当てはまりますが、作業する可能性のある各場所にイテレータを保存することを計画しているため、ほとんどの場合、直接到達できるスポットを操作し、リストをたどって取得することはほとんどありませんそれらのスポットに。

最初の例として、Webブラウザーを検討してください。Tabオブジェクトのリンクリストを保持し、各タブオブジェクトがブラウザで開いているタブに表示されるようにします。各タブは、数十メガバイトのデータである可能性があります(特にビデオのようなものが関係する場合)。開いているタブの一般的な数は簡単に12個未満で、おそらく100個が上限に近いでしょう。

2番目の例として、テキストを章のリンクリストとして保存するワードプロセッサを考えます。各章には、(たとえば)段落のリンクリストが含まれる場合があります。ユーザーが編集しているとき、通常、ユーザーは編集する特定の場所を見つけ、その場所(またはとにかくその段落内)でかなりの量の作業を行います。はい、彼らは時々一つの段落から別の段落に移動しますが、ほとんどの場合、それは彼らがすでに働いていた場所の近くの段落になります。

ときどき(グローバル検索や置換など)、すべてのリストのすべてのアイテムをたどることになりますが、それはかなり一般的ではありません。リストをたどる時間はほとんど重要ではありません。

典型的なケースでは、これは最初の基準にも適合する可能性が高いことに注意してください-チャプターにはかなり少ない数の段落が含まれており、各段落はかなり大きい可能性があります(少なくとも、ノードなど)。同様に、チャプタの数も比較的少なく、各チャプタは数キロバイト程度になる場合があります。

とはいえ、これらの例は両方ともおそらく少し工夫されていることを認めなければなりません。リンクリストはどちらに対しても十分に機能するかもしれませんが、どちらの場合も大きな利点はおそらくないでしょう。両方の場合で、例えば、いくつかの(空の)Webページ/タブまたはいくつかの空の章のいずれかにベクトルで余分なスペースを割り当てることは本当の問題ではないでしょう。


4
+1、ただし、ポインターを使用すると最初のケースが消えます。ポインターは常に大きなオブジェクトで使用する必要があります。リンクリストは、2番目の例にも適していません。配列は、それが短い場合、すべての操作に対して所有します。
アマラ

2
ラージオブジェクトの場合はまったく機能しません。std::vectorポインタの使用は、それらのすべてのリンクリストノードオブジェクトよりも効率的です。
ウィンストンユワート

リンクリストには多くの用途があります-動的配列ほど一般的ではないということだけです。LRUキャッシュは、リンクリストの一般的な使用方法の1つです。
チャールズサルビア

また、a std::vector<std::unique_ptr<T>>は良い代替手段かもしれません。
デュプリケータ

24

Bjarne Stroustrup自身によると、ベクトルは常にデータシーケンスのデフォルトコレクションである必要があります。要素の挿入と削除を最適化する場合はリストを選択できますが、通常は選択しないでください。リストのコストは、探索とメモリの使用が遅いことです。

彼はこのプレゼンテーションこれについて話します。

約0:44に、彼は一般的なベクトルとリストについて話します。

コンパクトさが重要です。ベクトルはリストよりもコンパクトです。そして、予測可能な使用パターンは非常に重要です。ベクトルを使用すると、多くの要素を突き出す必要がありますが、キャッシュは本当に優れています。...リストにはランダムアクセスがありません。ただし、リストを走査するときは、ランダムアクセスを続けます。ここにはノードがあり、メモリ内のそのノードに移動します。したがって、実際にはメモリにランダムにアクセスしており、キャッシュミスを最大化しています。これは、希望とは正反対です。

約1:08に、彼はこの問題について質問を受けます。

一連の要素が必要であることがわかります。そして、C ++の要素のデフォルトシーケンスはベクトルです。今、それはコンパクトで効率的だからです。実装、ハードウェアへのマッピングが重要です。さて、挿入と削除を最適化したい場合-あなたは言う、「まあ、私はシーケンスのデフォルトバージョンが欲しくありません。専門的なもの、つまりリストが必要です。そして、そうするなら、「私はいくつかのコストといくつかの問題、例えば、遅いトラバーサルとより多くのメモリ使用量を受け入れています」と言うのに十分知っている必要があります。


1
リンクするプレゼンテーションで「0:44および1:08頃」と言われていることを簡潔に書いていただけますか?
-gnat

2
@gnat-確かに。私は別に意味があり、スライドのコンテキストを必要とするものを引用しようとしました。
ピート

11

私が通常リストを使用する唯一の場所は、要素を消去し、イテレータを無効にしない場所です。std::vector挿入および消去時にすべての反復子を無効にします。std::list既存の要素のイテレータが挿入または削除後も有効であることを保証します。


4

既に提供されている他の回答に加えて、リストにはベクターには存在しない特定の機能があります(非常に高価なため)。スプライスとマージの操作が最も重要です。追加またはマージする必要のあるリストが頻繁にある場合は、おそらくリストが適切な選択です。

ただし、これらの操作を実行する必要がない場合は、おそらくそうではありません。


3

リンクリストに固有のキャッシュ/ページフレンドリネスがないため、多くのC ++開発者がそれらを完全に却下する傾向があり、そのデフォルト形式では正当な理由があります。

リンクされたリストはまだ素晴らしいことがあります

しかし、リンクされたリストは、本質的に不足している空間的局所性を戻す固定アロケーターに支えられている場合、素晴らしいものになります。

それらが優れているのは、たとえば、新しいポインタを保存し、1つまたは2つのポインタを操作するだけで、リストを2つのリストに分割できることです。ポインタを操作するだけでノードをあるリストから別のリストに一定時間で移動でき、空のリストは単一のheadポインタのメモリコストを単純に持つことができます。

シンプルなグリッドアクセラレータ

実際の例として、2D視覚シミュレーションを考えてみましょう。各フレームで動き回る数百万の粒子間の衝突検出などを加速するために使用される、400x400(160,000グリッドセル)に及ぶマップを備えたスクロール画面があります動的データ)。粒子の束はフレームごとに絶えず動き回っています。つまり、あるグリッドセルから別のグリッドセルに常に移動します。

この場合、各パーティクルが単一リンクリストノードである場合、各グリッドセルはをhead指すポインターとして開始できますnullptr。新しいパーティクルが生成されたら、headそのセルのポインタをこのパーティクルノードを指すように設定することで、そのパーティクルが存在するグリッドセルに配置します。パーティクルが1つのグリッドセルから次のグリッドセルに移動するときは、ポインタを操作するだけです。

これはvectors、各グリッドセルに160,000 を保存し、フレームごとに常に中央から押し戻して消去するよりもはるかに効率的です。

std :: list

ただし、これは、固定アロケーターに裏付けられた、手動で押し付けがましい、一重リンクのリスト用です。std::list二重リンクリストを表し、空の場合は単一のポインター(ベンダーの実装によって異なります)ほどコンパクトではない場合がありますstd::allocator。また、カスタムアロケーターをフォームに実装するのは少し苦痛です。

絶対に使用しlistないでください。しかし、リンクされたリストはまだ素晴らしいことがあります!しかし、それらは人々がしばしばそれらを使用するように誘惑される理由のために素晴らしいものではなく、少なくとも強制的なページフォールトと関連するキャッシュミスの多くを軽減する非常に効率的な固定アロケーターに支えられない限り、それほど素晴らしいものではありません。


1
C ++ 11以降には、標準の単一リンクリストがありstd::forward_listます。
sharyex

2

コンテナ内の要素のサイズを考慮する必要があります。

int ほとんどのデータがCPUキャッシュ内に収まるため、要素ベクトルは非常に高速です(SIMD命令はおそらくデータのコピーに使用できます)。

要素のサイズが大きい場合、テスト1と3の結果は大幅に変わる可能性があります。

以下からの非常に包括的な性能比較

これにより、各データ構造の使用に関する簡単な結論が導き出されます。

  • 数値演算:std::vectorまたはを使用std::deque
  • 線形検索:std::vectorまたはstd::deque
  • ランダム挿入/削除:
    • 小さいデータサイズ:使用 std::vector
    • 大きな要素サイズ:使用std::list(主に検索用でない限り)
  • 非自明なデータ型:std::list特に検索にコンテナが必要でない限り使用します。ただし、コンテナを複数回変更すると、非常に遅くなります。
  • 前に押す:std::dequeまたはstd::list

(サイドノートstd::dequeは非常に過小評価されているデータ構造です)。

利便性の観点から、std::list他の要素を挿入および削除するときにイテレータが無効にならないことを保証します。多くの場合、これは重要な側面です。


2

私の意見でリストを使用する最も顕著な理由は、イテレーターの無効化です:ベクターに要素を追加/削除すると、このベクターの特定の要素に保持されているすべてのポインター、参照、イテレーターが無効化され、微妙なバグにつながる可能性があります。 。またはセグメンテーション障害。

これはリストの場合ではありません。

すべての標準コンテナの正確なルールは、このStackOverflowの投稿に記載されています。


0

要するに、使用する正当な理由はありませんstd::list<>

  • 並べ替えられていないコンテナが必要な場合は、std::vector<>ルール。
    (要素をベクトルの最後の要素に置き換えて削除します。)

  • ソートされたコンテナが必要な場合は、std::vector<shared_ptr<>>ルール。

  • スパースインデックスが必要な場合は、std::unordered_map<>ルール。

それでおしまい。

リンクリストを使用する傾向があるのは1つの状況だけであることがわかります。追加のアプリケーションロジックを実装するために何らかの方法で接続する必要がある既存のオブジェクトがある場合。ただし、その場合std::list<>、特にほとんどのユースケースでは線形リストではなくツリーが生成されるため、オブジェクト内の(スマート)次ポインターに頼るのではなく、を使用しません。結果の構造はリンクされたリストである場合もあれば、ツリーまたは有向非循環グラフである場合もあります。これらのポインターの主な目的は、常に論理構造を構築することであり、オブジェクトを管理することではありません。そのstd::vector<>ためにあります。


-1

最初のテストで挿入をどのように行ったかを示す必要があります。2回目と3回目のテストでは、ベクターが簡単に勝ちます。

リストの重要な用途は、反復中のアイテムの削除をサポートする必要がある場合です。ベクトルが変更されると、すべての反復子が(潜在的に)無効になります。リストでは、削除された要素の反復子のみが無効です。他のすべての反復子は有効なままです。

コンテナの一般的な使用順序は、vector、deque、listです。コンテナの選択は通常、push_back、vector、pop_front、deque、selectリストの選択に基づいています。


3
反復中にアイテムを削除する場合、通常はベクターを使用して、結果用の新しいベクターを作成することをお
勧め

-1

私が考えることができる1つの要因は、ベクターが成長するにつれて、ベクターがそのメモリーの割り当てを解除し、より大きなブロックを何度も割り当てると、空きメモリーが断片化することです。これはリストの問題ではありません。

これは、push_backリザーブなしの多数のsが各サイズ変更中にコピーを引き起こし、非効率になるという事実に加えてです。同様に中央に挿入すると、すべての要素が右に移動し、さらに悪化します。

しかし、これが主要な関心事であるかどうかはわかりませんが、ベクターを避けるために私の仕事(モバイルゲーム開発)で与えられた理由でした。


1
いいえ、ベクターはコピーされ、それは高価です。しかし、リンクリストを(挿入する場所を見つけるために)探索するのも費用がかかります。キーは確かに測定することです
ケイトグレゴリー

それに加えて、意味@KateGregory私は、それに応じて私の編集をしてみましょう
カルティクT

3
正しいが、あなたが言及しなかったコストを信じるかどうか(そしてほとんどの人は信じない)、リンクされたリストを走査してそれらのコピーのOUTWEIGHSを挿入する場所を見つける(特に要素が小さい場合(または移動可能な場合)彼らは動きです))、そしてベクトルはしばしば(あるいは通常)高速です。信じようと信じまいと。
ケイトグレゴリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.