web-dev-qa-db-ja.com

C ++の効率的なリンクリスト?

この ドキュメントstd::listは非効率的です:

std :: listは、非常に非効率的なクラスであり、ほとんど役に立ちません。挿入されたすべての要素に対してヒープ割り当てを実行するため、特に小さなデータ型の場合、非常に高い一定の係数を持ちます。

コメント:それは驚きです。 std::listは二重にリンクされたリストであるため、要素構築の非効率性にもかかわらず、O(1)時間の複雑さで挿入/削除をサポートしますが、この機能はこの引用段落では完全に無視されます。

私の質問:小さい同種のsequentialコンテナが必要だと言う要素、およびこのコンテナは要素O /(1)の挿入/削除をサポートする必要があり、notランダムアクセスが必要です(ランダムアクセスのサポートはいいですが、ここでは必須ではありません)。また、少なくとも要素の数が少ない場合、各要素の構成のヒープ割り当てによって導入される高い定数係数は望ましくありません。最後に、iteratorsは、対応する要素が削除された場合にのみ無効にする必要があります。どうやら、カスタムコンテナクラスが必要になります。これは、二重リンクリストのバリアントである場合もあればそうでない場合もあります。このコンテナをどのように設計すればよいですか?

前述の仕様を達成できない場合、カスタムメモリーアロケーター、たとえばバンプポインターアロケーターが必要でしょうか?知っている std::listは、2番目のテンプレート引数としてアロケーターを取ります。

編集:私は、エンジニアリングの観点から、この問題にあまり関心を向けるべきではないことを知っています-十分な速さで十分です。これは単なる仮説的な質問であるため、詳細な使用例はありません。いくつかの要件を緩和してください!

Edit2:[〜#〜] o [〜#〜](1)の2つのアルゴリズムは、一定の要因の違いにより、パフォーマンスがまったく異なる場合があることを理解しています。

42
Leedehai

要件はexactlystd::listの要件ですが、ノードベースの割り当てのオーバーヘッドが気に入らないと判断した場合を除きます。

正しいアプローチは、最初から始めて、本当に必要なことだけを行うことです。

  1. std::listを使用してください。

    ベンチマーク:デフォルトのアロケーターは本当にあなたの目的には遅すぎるのですか?

    • いいえ:完了です。

    • はい:goto 2

  2. Boostプールアロケーターなどの既存のカスタムアロケーターでstd::listを使用する

    ベンチマーク:Boostプールアロケーターは本当にあなたの目的には遅すぎますか?

    • いいえ:完了です。

    • はい:goto 3

  3. std::listを使用して、手順1と2で行ったすべてのプロファイリングに基づいて、独自のニーズに合わせて細かく調整された手動のカスタムアロケーターを使用します

    以前と同様のベンチマークなど.

  4. 最後の手段として、もっとエキゾチックなことを検討してください。

    この段階に到達する場合、really十分に指定されたSOの質問、あなたが必要とするものについての多くの詳細を含む必要があります。 squeeze n nodes to cacheline」ではなく、「このドキュメントでは、この処理は遅く、音が悪い」と述べています。


PS。上記は2つの仮定を行いますが、両方とも調査する価値があります。

  1. baum mit Augenが指摘しているように、単純なエンドツーエンドのタイミングを実行するだけでは十分ではありません。時間の経過を確認する必要があるからです。アロケータ自体、またはメモリレイアウトなどによるキャッシュミスの可能性があります。何かが遅い場合でも、何を変更すべきかを知る前にwhyを確認する必要があります。
  2. 要件は当然のことですが、要件を弱める方法を見つけることが、何かをより速くするための最も簡単な方法であることがよくあります。

    • あなたは本当にどこでも、前面または背面、あるいはその両方で、途中ではなく、一定時間の挿入と削除が本当に必要ですか?
    • これらのイテレータ無効化制約が本当に必要ですか、それとも緩和できますか?
    • 悪用できるアクセスパターンはありますか?前面から要素を頻繁に削除してから新しい要素に置き換える場合は、その場で更新するだけですか?
87
Useless

別の方法として、拡張可能な配列を使用し、配列へのインデックスとしてリンクを明示的に処理できます。

未使用の配列要素は、いずれかのリンクを使用してリンクリストに配置されます。要素が削除されると、その要素は空きリストに返されます。空きリストが使い果たされたら、配列を大きくし、次の要素を使用します。

新しい無料の要素には、2つのオプションがあります。

  • それらを一度に無料リストに追加し、
  • フリーリスト内の要素数と配列サイズに基づいて、必要に応じて追加します。
18
Yves Daoust

削除されるノード上のイテレータ以外のイテレータを無効にしないという要件は、個々のノードを割り当てないすべてのコンテナに対して禁止されており、たとえばlistまたはmap
しかし、ほとんどすべてのケースで思考が必要であることがわかりました。できるかどうかを確認することをお勧めします。大きなメリットがあります。

リストのようなものが必要な場合、_std::list_は確かに「正しい」ものですが(ほとんどの場合、CSクラスの場合)、それはほとんど常に間違った選択であるというステートメントは、不運にも正確に正しいです。 O(1)クレームは完全に真ですが、それでも実際のコンピューターハードウェアの動作に関してはひどいものであり、非常に一定の要因となります。反復するオブジェクトだけでなく、ランダムに配置されますが、維持するノードもそうです(はい、どうにかしてアロケーターで回避できますが、それはポイントではありません)。  あなたがしたことの1つが保証されたキャッシュミス 2つまで 操作を変更するための1つの動的割り当て(1つはオブジェクト用、もう1つはノード用)。

編集:以下の@ratchetfreakが指摘したように、_std::list_の実装は、一般的にオブジェクトとノードの割り当てを最適化として1つのメモリブロックにまとめます(例:_make_shared_と同様) 、これにより、平均的なケースの壊滅的被害が若干少なくなります(one突然変異ごとの割り当てと、2つではなく1つのキャッシュミスの保証)。
この場合の新しい、異なる考慮事項は、そうすることも完全にトラブルフリーではないかもしれないということかもしれません。 2つのポインターでオブジェクトを後置することは、自動プリフェッチに干渉する可能性のある逆参照中に方向を逆にすることを意味します。
一方、オブジェクトの前にポインタを置くと、オブジェクトを2つのポインタのサイズだけ押し戻すことになります。これは、64ビットシステムでは最大16バイトを意味します。キャッシュラインの境界を超えるサイズのオブジェクトを毎回)。また、_std::list_は、たとえばSSEコードは、特別な驚きとして秘密のオフセットを追加するためだけのコードです(たとえば、xor-trickは、2ポインターのフットプリントの削減にはおそらく適用されません)。リストに追加されたオブジェクトが本来どおりに機能することを確認するための「安全な」パディングの量。
これらが実際のパフォーマンスの問題なのか、単に私の側からの不信と恐怖なのかはわかりませんが、予想よりも多くのヘビが草の中に隠れている可能性があると言ってもいいと思います。

有名なC++の専門家(特にStroustrup)が、_std::vector_を使用することをお勧めするのは、特に理由がない限りです。

以前の多くの人々のように、私はあなたがよりうまくいくことができると思われる特定の特別な問題のために_std::vector_よりも良いものを使用する(または発明する)ことをスマートにしようとしましたが、それは単にほとんどの場合、_std::vector_は常に最良または2番目に最適なオプションです(_std::vector_がたまに最適でない場合は、通常、代わりに_std::deque_が必要です)。
他のアプローチに比べて割り当てがはるかに少なく、メモリの断片化が少なく、間接性がはるかに少なく、より好ましいメモリアクセスパターンがあります。そして、それは容易に入手可能であり、機能していると思います。
挿入するたびにすべての要素のコピーが必要になるという事実は、(通常)まったく問題ではありません。あなたと思うそれはそうですが、そうではありません。まれにしか発生せず、メモリの線形ブロックのコピーです。これは、プロセッサが得意とするものです(多くの二重間接およびメモリ上のランダムなジャンプとは対照的です)。

イテレータを無効にしないという要件が本当に絶対必要である場合、たとえば、オブジェクトの_std::vector_を動的ビットセットとペアにするか、より良いものがない場合は_std::vector<bool>_とペアにすることができます。次に、reserve()を適切に使用して、再割り当てが行われないようにします。要素を削除するときは、削除せず、ビットマップで削除済みとしてマークするだけです(デストラクタを手動で呼び出します)。適切なタイミングで、イテレータを無効にできることがわかったら、ビットベクトルとオブジェクトベクトルの両方を圧縮する「バキュームクリーナー」関数を呼び出します。そこでは、予期しないイテレータの無効化がすべてなくなりました。

はい、それは余分な「要素が削除された」ビットを維持する必要があり、これは迷惑です。ただし、_std::list_は、実際のオブジェクトに加えて、2つのポインターも保持する必要があり、割り当てを行う必要があります。ベクター(または2つのベクター)を使用すると、キャッシュフレンドリーな方法で発生するため、アクセスは依然として非常に効率的です。削除されたノードをチェックする場合でも、繰り返しは、メモリ上を直線的またはほぼ直線的に移動することを意味します。

18
Damon

std::listは二重にリンクされたリストであるため、要素構築の非効率性にもかかわらず、挿入/削除O(1)時間の複雑さですが、これはこの引用された段落では、機能は完全に無視されます。

無視されます嘘だから

アルゴリズムの複雑さの問題は、一般的に1つのことを測定することです。たとえば、std::mapへの挿入がO(log N)であると言うとき、O(log N)comparisonsを実行することを意味します。 iteratingメモリからキャッシュラインをフェッチなどのコストは考慮されません。

これにより、もちろん分析が大幅に簡素化されますが、残念ながら、実際の実装の複雑さに必ずしもきれいに対応するわけではありません。特に、重大な仮定の1つは、メモリ割り当てが一定時間であるです。そして、それは大胆な嘘です。

汎用メモリアロケータ(mallocおよびco)には、メモリ割り当ての最悪の複雑さに関する保証はありません。最悪の場合は一般にOSに依存し、Linuxの場合はOOMキラーが関与する場合があります(進行中のプロセスをふるいにかけ、プロセスを強制終了してメモリを解放します)。

特別な目的のメモリアロケータは、特定の範囲の割り当て数(または最大割り当てサイズ)内で一定の時間になる可能性があります。 Big-O表記は無限大の限界に近いため、O(1)と呼ぶことはできません。

したがって、ゴムが道路に出会う場所std::listの実装は一般的に機能しませんO(1)挿入/削除理想的なものではなく、実際のメモリアロケータに依存しています。


これはかなり憂鬱ですが、すべての希望を失う必要はありません。

最も注目すべきは、要素数の上限を把握し、その量のメモリを事前に割り当てることができる場合、can一定時間のメモリ割り当てを実行するメモリアロケータを作成し、 O(1)の錯覚を与えます。

16
Matthieu M.

2つのstd::listsを使用します。1つは起動時に大量のノードのスタッシュが事前に割り当てられた「フリーリスト」、もう1つはフリーリストからspliceノードを入れる「アクティブ」リストです。これは一定の時間であり、ノードを割り当てる必要はありません。

7
Mark B

新しい slot_map プロポーザルの要求O(1)挿入および削除用。

video へのリンクもあり、実装の提案と以前の作業が記載されています。

要素の実際の構造についてもっと知っていれば、はるかに優れた特殊な連想コンテナがあるかもしれません。

5
Surt

フリーリストにリンクリストを使用する代わりに、ベクトルを使用することを除いて、@ Yves Daoustが言うことを正確に行うことをお勧めします。ベクターの背面にある無料のインデックスをプッシュおよびポップします。これは償却されますO(1)挿入、ルックアップ、および削除、およびポインター追跡を必要としません。また、面倒なアロケータービジネスを必要としません。

4
Dan

私はあなたの選択について小さなコメントをしたかっただけです。読み取り速度のため、私はベクターの大ファンです。任意の要素に直接アクセスし、必要に応じて並べ替えを行うことができます。 (たとえば、クラス/構造のベクトル)。

しかし、とにかく脱線、私が開示したかった2つの気の利いたヒントがあります。ベクトル挿入の場合は高価になる可能性があるため、巧妙なトリックです。挿入しない場合は挿入しないでください。通常のPush_back(最後に配置)を実行してから、必要な要素と要素を交換します。

削除についても同じです。それらは高価です。それを最後の要素と交換し、削除します。

2
ViperG

@Uselessの2番目の回答、特に要件の修正に関するPS項目2。イテレータの無効化制約を緩和する場合、std::vector<>の使用は、少数のアイテムコンテナに対して Stroustrupの標準提案 です(既にコメントで述べた理由のため)。 関連質問 SO.

C++ 11以降では、std::forward_listもあります。

また、コンテナに追加された要素の標準的なヒープ割り当てが十分でない場合は、非常に慎重に正確な要件で見て、微調整する必要があると思います。

2
Pablo H

すべての答えをありがとう。これは、厳密ではありませんが、単純なベンチマークです。

// list.cc
#include <list>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        list<size_t> ln;
        for (size_t i = 0; i < 200; i++) {
            ln.insert(ln.begin(), i);
            if (i != 0 && i % 20 == 0) {
                ln.erase(++++++++++ln.begin());
            }
        }
    }
}

そして

// vector.cc
#include <vector>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        vector<size_t> vn;
        for (size_t i = 0; i < 200; i++) {
            vn.insert(vn.begin(), i);
            if (i != 0 && i % 20 == 0) {
                vn.erase(++++++++++vn.begin());
            }
        }
    }
}

このテストの目的は、std::listがExcelに対して-[〜#〜] o [〜#〜](1)挿入および消去で要求する内容をテストすることです。また、挿入/削除を要求する位置のため、このレースはstd::vectorに対して大きく偏っています。これは、後続のすべての要素をシフトする必要があるためです(したがって[〜#〜] o [〜#〜](n))、std::listはその必要はありません。

今、私はそれらをコンパイルします。

clang++ list.cc -o list
clang++ vector.cc -o vector

そして、ランタイムをテストします。結果は次のとおりです。

  time ./list
  ./list  4.01s user 0.05s system 91% cpu 4.455 total
  time ./vector
  ./vector  1.93s user 0.04s system 78% cpu 2.506 total

std::vectorが勝ちました。

最適化O3でコンパイルされ、std::vectorは引き続き勝ちます。

  time ./list
  ./list  2.36s user 0.01s system 91% cpu 2.598 total
  time ./vector
  ./vector  0.58s user 0.00s system 50% cpu 1.168 total

std::listは、要素のヒープ割り当てを呼び出す必要がありますが、std::vectorはヒープメモリをバッチで割り当てることができます(ただし、実装に依存する場合があります)。したがって、std::listの挿入/削除は[〜#〜] o [〜#〜](1)ですが、より高い定数係数。

this document says

std::vectorは愛され、尊敬されています。

[〜#〜] edit [〜#〜]std::dequeは、場合によってはさらに良くなります。少なくともこのタスクでは

// deque.cc
#include <deque>
using namespace std;

int main() {
    for (size_t k = 0; k < 1e5; k++) {
        deque<size_t> dn;
        for (size_t i = 0; i < 200; i++) {
            dn.insert(dn.begin(), i);
            if (i != 0 && i % 20 == 0) {
                dn.erase(++++++++++dn.begin());
            }
        }
    }
}

最適化なし:

./deque  2.13s user 0.01s system 86% cpu 2.470 total

O3で最適化:

./deque  0.27s user 0.00s system 50% cpu 0.551 total
1
Leedehai