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


93

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

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

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

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

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


2
사전에 어떤 문제가 있습니까? 귀하와 같은 경우에 사용하기위한 것입니다.
Kamarey 2009

4
@Kamarey : HashSet이 더 나은 옵션 일 수 있습니다.
Brian Rasmussen

HashSet은 제가 찾던 것입니다.
DSent 2009

답변:


159

존재 여부를 확인하는 경우 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) 작업으로, 더 빠른 검색 기능을 제공합니다.

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

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

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


13
백만 개의 항목이있는 목록에 대해 부적절한 것은 없다고 생각합니다. 단지 선형 검색을 계속 실행하고 싶지 않을 것입니다.
Will Dean

동의합니다. 목록이나 항목이 많은 배열에는 문제가 없습니다. 값을 스캔하지 마십시오.
Michael Krauklis 2010

8

답이 있다고 생각합니다! 예, 목록 (배열)의 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) ... 문제 없어! 낭비되는 메모리 사용을 피하십시오!


팁을 주셔서 감사합니다, 나 자신이 나쁜 권투 성능에 잡혔습니다. 사용자 정의 Contains구현은 내 사용 사례에 비해 훨씬 빠릅니다.
Lea Hayes

5

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

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

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


Dictionary <Type, integer>는 고유하지 않은 개체도 효과적으로 저장할 수 있습니다. 정수를 사용하여 중복 수를 계산합니다. 예를 들어 {a, b, a} 목록을 {a = 2, b = 1}로 저장합니다. 물론 그것은 안수를 잃는 것입니다.
MSalters


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 초, 대기열은 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>에 대한 세 번째 테스트 케이스를 방금 추가했습니다. 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)로 설정할 수도 있습니다. 어느 쪽이 더 적은 메모리, 빈 문자열 또는 부울을 사용하는지 모르겠습니다. 내 추측은 bool 일 것입니다.
TTT

사전에서 string참조와 bool값은 각각 32 비트 또는 64 비트 시스템에 대해 3 바이트 또는 7 바이트의 차이를 만듭니다. 그러나 각 항목의 크기는 각각 4 또는 8의 배수로 반올림됩니다. string과 사이의 선택 bool은 크기에 전혀 차이가 없을 수 있습니다. 빈 문자열 ""은 항상 정적 속성으로 이미 메모리에 존재 string.Empty하므로 사전에서 사용하든 사용하지 않든 차이가 없습니다. (그리고 그것은 어쨌든 다른 곳에서 사용된다.)
Wormbo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.