List <T> .Contains()は非常に遅いですか?


90

ジェネリックList.Contains()関数がなぜ遅いのか、誰かが私に説明できますか?

私にはList<long>約100万の数値があり、これらの数値内に特定の数値があるかどうかを常にチェックしているコードがあります。

私は同じこと使ってやってみましたDictionary<long, byte>Dictionary.ContainsKey()機能を、それが速くリストと比べて10〜20倍程度でした。

もちろん、その目的で辞書を使用するつもりはありません。辞書をそのように使用するためのものではなかったからです。

だから、ここ本当の問題は、のいずれかの選択肢がありますされList<T>.Contains()ますが、ほど奇抜ではありませんかDictionary<K,V>.ContainsKey()


2
辞書にはどのような問題がありますか?それはあなたのような場合に使用するためのものです。
カマレイ2009

4
@Kamarey:HashSetの方が適している場合があります。
Brian Rasmussen、

HashSetは私が探していたものです。
DSは2009

回答:


156

存在を確認するだけの場合はHashSet<T>、.NET 3.5が最良のオプションです。辞書のようなパフォーマンスですが、キーと値のペアはありません。値だけです。

    HashSet<int> data = new HashSet<int>();
    for (int i = 0; i < 1000000; i++)
    {
        data.Add(rand.Next(50000000));
    }
    bool contains = data.Contains(1234567); // etc

30

List.ContainsはO(n)操作です。

Dictionary.ContainsKeyはO(1)操作です。これは、オブジェクトのハッシュコードをキーとして使用するため、より高速な検索機能を提供します。

100万のエントリを含むリストを作成するのは良い考えではないと思います。Listクラスがその目的のために設計されたとは思いません。:)

たとえば、これらのMillonエンティティをRDBMSに保存して、そのデータベースでクエリを実行することはできませんか?

それが不可能な場合は、とにかく辞書を使用します。


13
100万項目のリストには不適切なものはないと思います。おそらく、リスト全体で線形検索を実行し続けたくないだけです。
ウィルディーン

同意しますが、リストや配列の数がこれほど多くても、問題はありません。値をスキャンしないでください。
マイケルクラウクリス

8

答えがあると思います!はい、リスト(配列)のContains()がO(n)であることは事実ですが、配列が短く、値型を使用している場合でも、非常に高速です。しかし、CLRプロファイラー(Microsoftからの無料ダウンロード)を使用して、Contains()が値を比較するために値をボックス化していることを発見しました。[注:これは.Net 2.0です。テストされていない他の.Netバージョン。]

これが完全なストーリーと解決策です。「VI」という列挙があり、VIオブジェクトのリスト(配列)の抽象型である「ValueIdList」というクラスを作成しました。元の実装は古代の.Net 1.1日であり、カプセル化されたArrayListを使用していました。最近発見した http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspxでで、ジェネリックリスト(List <VI>)がArrayListよりも値タイプ(列挙型VI)値をボックス化する必要がないため。それは本当であり、それはうまくいった...ほとんど。

CLRプロファイラーは驚きを明らかにしました。これは割り当てグラフの一部です:

  • ValueIdList :: Contains bool(VI)5.5MB(34.81%)
  • Generic.List :: Contains bool(<UNKNOWN>)5.5MB(34.81%)
  • Generic.ObjectEqualityComparer <T> :: Equals bool(<UNKNOWN> <UNKNOWN>)5.5MB(34.88%)
  • 値.VI 7.7MB(49.03%)

ご覧のように、Contains()は意外にもGeneric.ObjectEqualityComparer.Equals()を呼び出します。これは明らかに、高価なヒープ割り当てを必要とするVI値のボックス化を必要とします。マイクロソフトがリストのボクシングを削除するのは奇妙ですが、このような単純な操作でボクシングを再度要求するだけです。

私たちの解決策は、Contains()実装を書き直すことでした。この場合、すでに汎用リストオブジェクト(_items)をカプセル化しているため、これは簡単です。簡単なコードは次のとおりです。

public bool Contains(VI id) 
{
  return IndexOf(id) >= 0;
}

public int IndexOf(VI id) 
{ 
  int i, count;

  count = _items.Count;
  for (i = 0; i < count; i++)
    if (_items[i] == id)
      return i;
  return -1;
}

public bool Remove(VI id) 
{
  int i;

  i = IndexOf(id);
  if (i < 0)
    return false;
  _items.RemoveAt(i);

  return true;
}

VI値の比較は、ボクシングを必要としない独自のバージョンのIndexOf()で行われ、非常に高速です。私たちの特定のプログラムは、この単純な書き直し後に20%スピードアップしました。O(n)...問題ありません!無駄なメモリ使用量を避けてください!


ヒントをありがとう、私は悪いボクシングパフォーマンスに自分で捕まっていた。Contains私のユースケースでは、カスタム実装の方がはるかに高速です。
Lea Hayes

5

辞書のキーは高速に検出されるように設計されているため、辞書はそれほど悪くありません。リスト内の数値を見つけるには、リスト全体を反復処理する必要があります。

もちろん、辞書はあなたの番号が一意であり、順序付けされていない場合にのみ機能します。

HashSet<T>.NET 3.5にもクラスがあると思います。また、一意の要素のみを許可します。


Dictionary <Type、integer>は、一意でないオブジェクトも効果的に格納できます。整数を使用して重複の数を数えます。たとえば、リスト{a、b、a}を{a = 2、b = 1}として保存します。もちろん、序列は失われます。
MSalters 2009


2

これはあなたの質問に対する答えではありませんが、コレクションに対するContains()のパフォーマンスを向上させるクラスがあります。キューをサブクラス化し、ハッシュコードをオブジェクトのリストにマップするディクショナリを追加しました。Dictionary.Contains()関数はO(1)一方でList.Contains()Queue.Contains()およびStack.Contains()O(N)です。

辞書の値タイプは、同じハッシュコードを持つオブジェクトを保持するキューです。呼び出し元は、IEqualityComparerを実装するカスタムクラスオブジェクトを提供できます。このパターンをスタックまたはリストに使用できます。コードはほんの少しの変更が必要です。

/// <summary>
/// This is a class that mimics a queue, except the Contains() operation is O(1) rather     than O(n) thanks to an internal dictionary.
/// The dictionary remembers the hashcodes of the items that have been enqueued and dequeued.
/// Hashcode collisions are stored in a queue to maintain FIFO order.
/// </summary>
/// <typeparam name="T"></typeparam>
private class HashQueue<T> : Queue<T>
{
    private readonly IEqualityComparer<T> _comp;
    public readonly Dictionary<int, Queue<T>> _hashes; //_hashes.Count doesn't always equal base.Count (due to collisions)

    public HashQueue(IEqualityComparer<T> comp = null) : base()
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>();
    }

    public HashQueue(int capacity, IEqualityComparer<T> comp = null) : base(capacity)
    {
        this._comp = comp;
        this._hashes = new Dictionary<int, Queue<T>>(capacity);
    }

    public HashQueue(IEnumerable<T> collection, IEqualityComparer<T> comp = null) :     base(collection)
    {
        this._comp = comp;

        this._hashes = new Dictionary<int, Queue<T>>(base.Count);
        foreach (var item in collection)
        {
            this.EnqueueDictionary(item);
        }
    }

    public new void Enqueue(T item)
    {
        base.Enqueue(item); //add to queue
        this.EnqueueDictionary(item);
    }

    private void EnqueueDictionary(T item)
    {
        int hash = this._comp == null ? item.GetHashCode() :     this._comp.GetHashCode(item);
        Queue<T> temp;
        if (!this._hashes.TryGetValue(hash, out temp))
        {
            temp = new Queue<T>();
            this._hashes.Add(hash, temp);
        }
        temp.Enqueue(item);
    }

    public new T Dequeue()
    {
        T result = base.Dequeue(); //remove from queue

        int hash = this._comp == null ? result.GetHashCode() : this._comp.GetHashCode(result);
        Queue<T> temp;
        if (this._hashes.TryGetValue(hash, out temp))
        {
            temp.Dequeue();
            if (temp.Count == 0)
                this._hashes.Remove(hash);
        }

        return result;
    }

    public new bool Contains(T item)
    { //This is O(1), whereas Queue.Contains is (n)
        int hash = this._comp == null ? item.GetHashCode() : this._comp.GetHashCode(item);
        return this._hashes.ContainsKey(hash);
    }

    public new void Clear()
    {
        foreach (var item in this._hashes.Values)
            item.Clear(); //clear collision lists

        this._hashes.Clear(); //clear dictionary

        base.Clear(); //clear queue
    }
}

私の簡単なテストは、私HashQueue.Contains()よりもはるかに速く実行されることを示していますQueue.Contains()。カウントを10,000に設定してテストコードを実行すると、HashQueueバージョンの場合は0.00045秒、Queueバージョンの場合は0.37秒かかります。カウントが100,000の場合、HashQueueバージョンは0.0031秒かかりますが、Queueは36.38秒かかります。

これが私のテストコードです:

static void Main(string[] args)
{
    int count = 10000;

    { //HashQueue
        var q = new HashQueue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("HashQueue, {0}", sw.Elapsed));
    }

    { //Queue
        var q = new Queue<int>(count);

        for (int i = 0; i < count; i++) //load queue (not timed)
            q.Enqueue(i);

        System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            bool contains = q.Contains(i);
        }
        sw.Stop();
        Console.WriteLine(string.Format("Queue,     {0}", sw.Elapsed));
    }

    Console.ReadLine();
}

HashSet <T>の3つ目のテストケースを追加しました。これは、ソリューションよりも優れた結果が得られるようです HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
。– psulek

1

辞書が不適切なのはなぜですか?

特定の値がリストにあるかどうかを確認するには、リスト全体を調べる必要があります。辞書(またはその他のハッシュベースのコンテナー)を使用すると、比較する必要があるオブジェクトの数を絞り込むのがはるかに速くなります。キー(あなたの場合は、数)はハッシュされ、それによりディクショナリに比較するオブジェクトの部分サブセットが与えられます。


0

HashSetがサポートされていないCompact Frameworkでこれを使用しています。両方の文字列が探している値であるDictionaryを選択しました。

これは、辞書のパフォーマンスでlist <>機能を取得することを意味します。少しハッキーですが、動作します。


1
HashSetの代わりにディクショナリを使用している場合は、キーと同じ文字列ではなく、値を ""に設定することもできます。これにより、使用するメモリが少なくなります。あるいは、Dictionary <string、bool>を使用して、すべてをtrue(またはfalse)に設定することもできます。どちらがより少ないメモリ、空の文字列、またはブール値を使用するかわかりません。私の推測は真実でしょう。
TTT

辞書では、string参照とbool値はそれぞれ32または64ビットシステムで3または7バイトの違いを作ります。ただし、各エントリのサイズはそれぞれ4または8の倍数に切り上げられることに注意してください。したがって、stringとの選択でboolは、サイズにまったく違いがありません。空の文字列""は常に静的プロパティとして既にメモリに存在しているstring.Emptyので、辞書で使用するかどうかに関係なく、違いはありません。(そして、それはとにかくどこでも使用されます。)
Wormbo
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.