[.net] List <T> .Contains ()가 매우 느립니까?

제네릭 List.Contains()기능이 왜 그렇게 느린 지 설명해 주 시겠습니까?

나는이 List<long>만 약 번호,이 번호 내의 특정 번호가 있는지 지속적으로 확인되는 코드와 함께.

내가 사용하는 똑같은 일을 시도 Dictionary<long, byte>하고 Dictionary.ContainsKey()기능을, 그리고 10 ~ 20 배 빠른 목록보다 관하여이었다.

물론 그 목적으로 Dictionary를 사용하고 싶지는 않습니다. 왜냐하면 그런 식으로 사용하도록 의도 된 것이 아니었기 때문입니다.

그래서, 여기에 진짜 문제는 어떤 대안이 있습니다 List<T>.Contains()만, 한 괴팍스러운하지 Dictionary<K,V>.ContainsKey()?



답변

존재 여부를 확인하는 경우 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


답변

List.Contains는 O (n) 연산입니다.

Dictionary.ContainsKey는 객체의 해시 코드를 키로 사용하기 때문에 O (1) 작업으로, 더 빠른 검색 기능을 제공합니다.

백만 개의 항목을 포함하는 목록을 갖는 것은 좋은 생각이 아니라고 생각합니다. 나는 List 클래스가 그러한 목적으로 디자인되었다고 생각하지 않습니다. 🙂

예를 들어 이러한 수백만 엔터티를 RDBMS에 저장하고 해당 데이터베이스에서 쿼리를 수행하는 것이 가능하지 않습니까?

가능하지 않다면 어쨌든 사전을 사용하겠습니다.


답변

답이 있다고 생각합니다! 예, 목록 (배열)의 Contains ()가 O (n) 인 것은 사실이지만 배열이 짧고 값 유형을 사용하는 경우 여전히 매우 빠릅니다. 그러나 CLR 프로파일 러 [Microsoft에서 무료 다운로드]를 사용하여 Contains ()가 값을 비교하기 위해 boxing 값이라는 것을 발견했습니다.이 경우 힙 할당이 필요하며 이는 매우 비쌉니다 (느림). [참고 : 이것은 .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>)이 값 유형 (예 : 우리의 enum 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 %)
  • Values.VI 7.7MB (49.03 %)

보시다시피, Contains ()는 놀랍게도 Generic.ObjectEqualityComparer.Equals ()를 호출하는데, 이는 분명히 값 비싼 힙 할당이 필요한 VI 값의 boxing을 필요로합니다. Microsoft가 목록에서 권투를 제거하고 이와 같은 간단한 작업을 위해 다시 요구하는 것은 이상합니다.

우리의 해결책은 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 값의 비교는 이제 boxing이 필요없는 IndexOf ()의 자체 버전에서 수행되며 매우 빠릅니다. 우리의 특정 프로그램은이 간단한 재 작성 후에 20 % 나 빨라졌습니다. O (n) … 문제 없어! 낭비되는 메모리 사용을 피하십시오!


답변

사전은 그다지 나쁘지 않습니다. 사전의 키는 빠르게 찾을 수 있도록 설계 되었기 때문입니다. 목록에서 숫자를 찾으려면 전체 목록을 반복해야합니다.

물론 사전은 번호가 고유하고 순서가 지정되지 않은 경우에만 작동합니다.

HashSet<T>.NET 3.5 에도 클래스 가 있다고 생각 하며 고유 한 요소 만 허용합니다.


답변

SortedList는 (하지만 느린 항목을 삽입하는) 검색 빨라집니다


답변

이것은 귀하의 질문에 대한 답은 아니지만 컬렉션에서 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 초, 대기열은 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();
}


답변

사전이 부적절한 이유는 무엇입니까?

특정 값이 목록에 있는지 확인하려면 전체 목록을 살펴 봐야합니다. 딕셔너리 (또는 다른 해시 기반 컨테이너)를 사용하면 비교해야하는 객체 수를 좁히는 것이 훨씬 더 빠릅니다. 키 (귀하의 경우 숫자)는 해시되어 비교할 개체의 부분 하위 집합을 사전에 제공합니다.