私はC++のリストとベクトルを含む3つの異なる実験を実行しました。
途中に多くの挿入が含まれる場合でも、ベクトルを持つものはより効率的であることがわかりました。
したがって、質問:リストはベクトルよりも意味がありますか?
ベクトルがほとんどの場合により効率的であり、そのメンバーがどれほど似ているかを考えた場合、リストにはどのような利点がありますか?
N個の整数を生成し、それらをコンテナーに入れて、コンテナーがソートされたままになるようにします。挿入は、要素を1つずつ読み取り、新しい要素を最初の大きい要素の直前に挿入することにより、単純に実行されました。
リストを使用すると、ベクトルと比較して、次元が増加するときに時間が屋根を通過します。
コンテナの最後にN個の整数を挿入します。
リストとベクトルの場合、時間は同じ桁で増加しましたが、ベクトルの場合は3倍速くなりました。
コンテナにN個の整数を挿入します。
タイマーを開始します。
リストにはlist.sortを、ベクターにはstd :: sortを使用してコンテナを並べ替えます。タイマーを停止します。
再び、時間は同じ桁で増加しますが、ベクトルを使用すると平均で5倍速くなります。
私は引き続きテストを実行し、リストの方が優れていると思われるいくつかの例を見つけ出します。
しかし、このメッセージを読んでいる皆さんの共同体験は、より生産的な答えを提供する可能性があります。
リストの方が使いやすい、またはパフォーマンスが良い状況に出くわしたかもしれません。
簡単に言えば、症例は少なく、はるかに少ないようです。おそらくいくつかあります。
1つは、少数の大きなオブジェクトを保存する必要がある場合です。特に、オブジェクトが非常に大きいため、オブジェクトの数が少ない場合でもスペースを割り当てることは現実的ではありません。基本的に、ベクターまたは両端キューが余分なオブジェクトにスペースを割り当てるのを止める方法はありません-それはそれらが定義されている方法です(つまり、それらはmust複雑さの要件を満たすために追加のスペースを割り当てる)。フラットアウトする場合できないその余分なスペースの割り当てを許可すると、std::list
は、必要に応じてonly標準コンテナにすることができます。
もう1つは、イテレータをリスト内の「興味深い」ポイントに長期間保存する場合であり、挿入や削除を行う場合は、(ほぼ)常に次の場所から実行します。すでにイテレータがあるので、挿入または削除を行うポイントに到達するためにリストをウォークスルーする必要はありません。複数のスポットで作業する場合も同じことが明らかですが、作業する可能性の高い場所ごとにイテレータを格納することを計画しているため、直接到達できるスポットをほとんど操作し、リストをたどってめったに取得しません。それらのスポットに。
最初の例として、Webブラウザーを検討してください。 Tab
オブジェクトのリンクリストを保持し、各タブオブジェクトがブラウザーで開いているタブを表す場合があります。各タブは数十メガバイトのデータになる場合があります(特に、ビデオのようなものが含まれている場合はさらに多く)。開いているタブの典型的な数は、ダースよりも簡単に少なくなる可能性があり、100はおそらく上限に近いです。
2番目の例として、テキストを章のリンクリストとして保存するワードプロセッサを考えてください。各章には、リンクされた(たとえば)段落のリストが含まれている場合があります。ユーザーが編集しているとき、ユーザーは通常、編集する特定の場所を見つけ、その場所(またはその段落内)でかなりの量の作業を行います。はい、それらは時々パラグラフから別のパラグラフに移動しますが、ほとんどの場合それは彼らがすでに働いていた場所の近くのパラグラフになります。
たまに(グローバル検索と置換のようなもの)すべてのリストのすべての項目をたどることになりますが、それはかなりまれであり、そうしたとしても、おそらく項目内で項目内を検索するのに十分な作業を行うでしょうリストをトラバースする時間はほとんど重要ではありません。
典型的なケースでは、これは最初の基準にも当てはまることに注意してください-章にはかなり少数の段落が含まれ、各段落はかなり大きくなる可能性があります(少なくともノード内のポインタのサイズなど)。同様に、比較的少数のチャプターがあり、それぞれが数キロバイト程度になる場合があります。
とはいえ、これらの例はどちらもおそらく少し工夫されていることを認めなければなりません。リンクされたリストはどちらでも完全に機能する可能性がありますが、どちらの場合も大きな利点はありません。どちらの場合も、たとえば、一部の(空の)Webページ/タブまたは一部の空のチャプターのいずれかにベクター内に余分なスペースを割り当てることは、実際の問題にはなりそうにありません。
Bjarne Stroustrup氏自身によれば、ベクターは常に一連のデータのデフォルトのコレクションである必要があります。要素の挿入と削除を最適化する場合はリストを選択できますが、通常はそうするべきではありません。リストのコストは、トラバーサルとメモリ使用量が遅いことです。
彼はこれについて このプレゼンテーション で話します。
0:44頃に、彼は一般的にベクターとリストについて話します。
コンパクトさが重要です。ベクトルはリストよりコンパクトです。そして、予測可能な使用パターンは非常に重要です。ベクトルを使用すると、多くの要素を駆除する必要がありますが、キャッシュはその点で本当に優れています。 ...リストにはランダムアクセスがありません。しかし、リストをトラバースするときは、ランダムアクセスを続けます。ここにノードがあり、それはメモリ内のそのノードに行きます。したがって、実際にはメモリにランダムにアクセスし、キャッシュミスを最大化します。これは、希望とは正反対です。
1:08頃、彼はこの問題について質問されます。
一連の要素が必要であることがわかります。また、C++のデフォルトの要素シーケンスはベクトルです。それがコンパクトで効率的だからです。実装、ハードウェアへのマッピング、問題。さて、挿入と削除のために最適化したい場合-あなたは「まあ、私はシーケンスのデフォルトバージョンを望んでいません。専用のリストが欲しいです。それを行う場合は、「トラバーサルが遅くなり、メモリ使用量が増えるなど、いくつかのコストと問題を受け入れています」と言うのに十分な知識が必要です。
私が通常リストを使用する唯一の場所は、要素を消去し、イテレータを無効にしない必要がある場合です。 std::vector
挿入および消去時にすべての反復子を無効にします。 std::list
は、既存の要素への反復子が挿入または削除後も有効であることを保証します。
すでに提供されている他の回答に加えて、リストにはベクトルには存在しない特定の機能があります(非常に高価になるためです)。スプライスとマージの操作が最も重要です。一緒に追加またはマージする必要のある一連のリストが頻繁にある場合、リストはおそらく良い選択です。
ただし、これらの操作を実行する必要がない場合は、おそらく不要です。
リンクリストに固有のキャッシュ/ページの使いやすさの欠如は、多くのC++開発者によってそれらをほとんど完全に却下する傾向があり、そのデフォルト形式では正当化されます。
リンクされたリストはまだ素晴らしいことができます
しかし、リンクされたリストは素晴らしいになる可能性があります。固定アロケータによって、本来欠けている空間的な局所性をそれらに戻すことができます。
Excelの場合、たとえば、新しいポインタを格納して1つまたは2つのポインタを操作するだけで、リストを2つのリストに分割できます。単なるポインタ操作によって、あるリストから別のリストにノードを一定の時間で移動できます。空のリストは、単一のhead
ポインタのメモリコストを持つことができます。
シンプルグリッドアクセラレータ
実用的な例として、2Dビジュアルシミュレーションを考えます。 400x400(160,000グリッドセル)にわたるマップを備えたスクロール画面があり、各フレームで移動する何百万ものパーティクル間の衝突検出などを加速するために使用されます(ここではクワッドツリーは避けます。動的データ)。粒子の束全体が各フレームで常に動き回っています。つまり、1つのグリッドセルに常駐していたものから、別のグリッドセルに常駐しているということです。
この場合、各パーティクルが単一リンクリストノードである場合、各グリッドセルは、head
を指すnullptr
ポインターとして開始することができます。新しいパーティクルが生成されたら、そのセルのhead
ポインターをこのパーティクルノードを指すように設定することで、それをグリッドセルに配置します。粒子があるグリッドセルから次のセルに移動するときは、ポインターを操作するだけです。
これは、グリッドセルごとに160,000個のvectors
を保存し、フレームごとに常に中央から押し戻して消去するよりもはるかに効率的です。
std :: list
これは、固定アロケータに裏打ちされた、手作業で挿入された、単方向にリンクされたリスト用です。 std::list
は二重にリンクされたリストを表し、空の場合は単一のポインター(ベンダーの実装によって異なる)ほどコンパクトではない可能性があり、さらにstd::allocator
形式でカスタムアロケーターを実装するのは簡単ではありません。
私はlist
を一切使用しないことを認めなければなりません。しかし、リンクされたリストはまだ素晴らしいです!それでも、人々がそれらを使用するように誘惑されることが多いという理由から、それらは素晴らしいものではなく、少なくとも強制的なページフォールトと関連するキャッシュミスの多くを軽減する非常に効率的な固定アロケータに支えられていない限り、それほど素晴らしいものではありません。
コンテナ内の要素のサイズを考慮する必要があります。
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
は、他の要素を挿入および削除するときに反復子が無効化されないことを保証します。多くの場合、それは重要な側面です。
私の考えではリストを使用する最も顕著な理由はiterator invalidationです:ベクトルに要素を追加/削除すると、すべてのポインター、参照、イテレーターこのベクターの特定の要素を保持すると、無効になり、微妙なバグやセグメンテーションエラーが発生する可能性があります。
これはリストには当てはまりません。
すべての標準コンテナーの正確なルールは このStackOverflowポストで指定 です。
つまり、std::list<>
を使用する正当な理由はありません。
並べ替えられていないコンテナが必要な場合は、std::vector<>
ルール。
(要素をベクターの最後の要素で置き換えることにより要素を削除します。)
ソートされたコンテナが必要な場合は、std::vector<shared_ptr<>>
ルール。
スパースインデックスが必要な場合は、std::unordered_map<>
ルール。
それでおしまい。
リンクリストを使用する状況は1つだけあることに気づきました。追加のアプリケーションロジックを実装するために何らかの方法で接続する必要がある既存のオブジェクトがある場合です。ただし、その場合はstd::list<>
を使用することはなく、オブジェクト内の(スマートな)次のポインターに頼っています。結果の構造はリンクされたリストである場合もあれば、ツリーまたは有向非循環グラフである場合もあります。これらのポインタの主な目的は、常に論理構造を構築することであり、オブジェクトを管理することではありません。そのためのstd::vector<>
があります。