Danh sách <T> .Contains () rất chậm?


93

Bất cứ ai có thể giải thích cho tôi tại sao List.Contains()chức năng generics chậm như vậy?

Tôi có một List<long>với khoảng một triệu số và mã liên tục kiểm tra xem có một số cụ thể trong những số này hay không.

Tôi đã thử làm điều tương tự bằng cách sử dụng Dictionary<long, byte>Dictionary.ContainsKey()hàm, và nó nhanh hơn khoảng 10-20 lần so với List.

Tất nhiên, tôi không thực sự muốn sử dụng Từ điển cho mục đích đó, bởi vì nó không được sử dụng theo cách đó.

Vì vậy, câu hỏi thực sự ở đây là, có cách nào thay thế cho List<T>.Contains(), nhưng không khó như Dictionary<K,V>.ContainsKey()vậy không?


2
Vấn đề gì với Từ điển? Nó được thiết kế để sử dụng trong trường hợp như của bạn.
Kamarey

4
@Kamarey: HashSet có thể là một lựa chọn tốt hơn.
Brian Rasmussen

HashSet là thứ tôi đang tìm kiếm.
DSent

Câu trả lời:


159

Nếu bạn chỉ đang kiểm tra sự tồn tại, HashSet<T>trong .NET 3.5 là lựa chọn tốt nhất của bạn - hiệu suất giống như từ điển, nhưng không có cặp khóa / giá trị - chỉ các giá trị:

    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 là một phép toán O (n).

Dictionary.ContainsKey là một hoạt động O (1), vì nó sử dụng mã băm của các đối tượng làm khóa, mang lại cho bạn khả năng tìm kiếm nhanh hơn.

Tôi không nghĩ rằng đó là một ý kiến ​​hay nếu có một Danh sách chứa hàng triệu mục nhập. Tôi không nghĩ rằng lớp Danh sách được thiết kế cho mục đích đó. :)

Chẳng hạn, không thể lưu các thực thể millon đó vào một RDBMS và thực hiện các truy vấn trên cơ sở dữ liệu đó?

Nếu không thể, thì dù sao tôi cũng sẽ sử dụng Từ điển.


13
Tôi không nghĩ rằng có điều gì không phù hợp về một danh sách với hàng triệu mục, chỉ là bạn có thể không muốn tiếp tục chạy các tìm kiếm tuyến tính trên đó.
Will Dean

Đồng ý, không có gì sai với một danh sách cũng như một mảng với nhiều mục nhập như vậy. Chỉ cần không quét các giá trị.
Michael Krauklis

8

Tôi nghĩ rằng tôi có câu trả lời! Đúng, đúng là Chứa () trên một danh sách (mảng) là O (n), nhưng nếu mảng ngắn và bạn đang sử dụng các kiểu giá trị, thì nó vẫn sẽ khá nhanh. Nhưng bằng cách sử dụng CLR Profiler [tải xuống miễn phí từ Microsoft], tôi phát hiện ra rằng Contains () là các giá trị quyền anh để so sánh chúng, điều này yêu cầu phân bổ heap, điều này RẤT đắt (chậm). [Lưu ý: Đây là .Net 2.0; các phiên bản .Net khác chưa được kiểm tra.]

Đây là toàn bộ câu chuyện và giải pháp. Chúng ta có một kiểu liệt kê được gọi là "VI" và tạo một lớp gọi là "ValueIdList" là một kiểu trừu tượng cho danh sách (mảng) các đối tượng VI. Quá trình triển khai ban đầu là trong .Net 1.1 ngày xưa và nó sử dụng ArrayList được đóng gói. Gần đây, chúng tôi đã phát hiện ra trong http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx rằng danh sách chung (Danh sách <VI>) hoạt động tốt hơn nhiều so với ArrayList trên các loại giá trị (như của chúng tôi enum VI) bởi vì các giá trị không cần phải được đóng hộp. Đó là sự thật và nó đã hoạt động ... gần như vậy.

Hồ sơ CLR tiết lộ một điều bất ngờ. Đây là một phần của Biểu đồ phân bổ:

  • ValueIdList :: Chứa bool (VI) 5,5MB (34,81%)
  • Generic.List :: Chứa bool (<UNKNOWN>) 5,5MB (34,81%)
  • Generic.ObjectEqualityComparer <T> :: Equals bool (<UNKNOWN> <UNKNOWN>) 5,5MB (34,88%)
  • Giá trị.VI 7,7MB (49,03%)

Như bạn có thể thấy, Contains () gọi Generic.ObjectEqualityComparer.Equals () một cách đáng ngạc nhiên, rõ ràng yêu cầu quyền chọn giá trị VI, yêu cầu phân bổ heap đắt tiền. Thật kỳ lạ khi Microsoft loại bỏ quyền anh trong danh sách, chỉ yêu cầu lại quyền anh cho một hoạt động đơn giản như thế này.

Giải pháp của chúng tôi là viết lại triển khai Contains (), điều này trong trường hợp của chúng tôi rất dễ thực hiện vì chúng tôi đã đóng gói đối tượng danh sách chung (_items). Đây là mã đơn giản:

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ệc so sánh các giá trị VI hiện đang được thực hiện trong phiên bản IndexOf () của riêng chúng tôi, không yêu cầu quyền anh và nó rất nhanh. Chương trình cụ thể của chúng tôi đã tăng tốc 20% sau khi viết lại đơn giản này. O (n) ... không sao! Chỉ cần tránh sử dụng bộ nhớ lãng phí!


Cảm ơn vì lời khuyên, tôi đã bị bắt bởi màn biểu diễn quyền anh tệ hại. ContainsTriển khai tùy chỉnh nhanh hơn cho trường hợp sử dụng của tôi.
Lea Hayes

5

Từ điển không tệ lắm, vì các khóa trong từ điển được thiết kế để tìm nhanh. Để tìm một số trong danh sách, nó cần phải lặp lại toàn bộ danh sách.

Tất nhiên từ điển chỉ hoạt động nếu các số của bạn là duy nhất và không có thứ tự.

Tôi nghĩ rằng cũng có một HashSet<T>lớp trong .NET 3.5, nó cũng chỉ cho phép các phần tử duy nhất.


Từ điển <Kiểu, số nguyên> cũng có thể lưu trữ hiệu quả các đối tượng không phải là duy nhất - sử dụng số nguyên để đếm số lượng bản sao. Ví dụ: bạn lưu trữ danh sách {a, b, a} dưới dạng {a = 2, b = 1}. Tất nhiên, nó làm mất đi thử thách.
MSalters


2

Đây không phải là câu trả lời chính xác cho câu hỏi của bạn, nhưng tôi có một lớp giúp tăng hiệu suất của Contains () trên một tập hợp. Tôi xếp lớp con một Hàng đợi và thêm một Từ điển ánh xạ mã băm vào danh sách các đối tượng. Các Dictionary.Contains()chức năng là O (1) trong khi List.Contains(), Queue.Contains()Stack.Contains()là O (n).

Kiểu giá trị của từ điển là một hàng đợi chứa các đối tượng có cùng một mã băm. Người gọi có thể cung cấp một đối tượng lớp tùy chỉnh triển khai IEqualityComparer. Bạn có thể sử dụng mẫu này cho Ngăn xếp hoặc Danh sách. Mã sẽ chỉ cần một vài thay đổi.

/// <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
    }
}

Thử nghiệm đơn giản của tôi cho thấy rằng tôi HashQueue.Contains()chạy nhanh hơn nhiều Queue.Contains(). Chạy mã thử nghiệm với số đếm được đặt thành 10.000 mất 0.00045 giây đối với phiên bản HashQueue và 0.37 giây đối với phiên bản Hàng đợi. Với số lượng 100.000, phiên bản HashQueue mất 0,0031 giây trong khi Hàng đợi mất 36,38 giây!

Đây là mã thử nghiệm của tôi:

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();
}

Tôi chỉ cần thêm trường hợp thử nghiệm thứ 3 cho HashSet <T> mà dường như để có được kết quả thậm chí tốt hơn so với giải pháp của bạn: HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek

1

Tại sao một từ điển không phù hợp?

Để xem liệu một giá trị cụ thể có trong danh sách hay không, bạn cần xem toàn bộ danh sách. Với từ điển (hoặc vùng chứa dựa trên băm khác), việc thu hẹp số lượng đối tượng bạn cần so sánh sẽ nhanh hơn nhiều. Khóa (trong trường hợp của bạn là số) được băm và điều đó cung cấp cho từ điển tập hợp con phân số của các đối tượng để so sánh với.


0

Tôi đang sử dụng cái này trong Khung nhỏ gọn, nơi không có hỗ trợ cho HashSet, tôi đã chọn Từ điển trong đó cả hai chuỗi là giá trị tôi đang tìm kiếm.

Nó có nghĩa là tôi nhận được danh sách <> chức năng với hiệu suất từ ​​điển. Nó hơi hacky, nhưng nó hoạt động.


1
Nếu bạn đang sử dụng Từ điển thay cho HashSet, bạn cũng có thể đặt giá trị thành "" thay vì cùng một chuỗi với khóa. Bằng cách đó, bạn sẽ sử dụng ít bộ nhớ hơn. Ngoài ra, bạn thậm chí có thể sử dụng Dictionary <string, bool> và đặt tất cả chúng thành true (hoặc false). Tôi không biết cái nào sẽ sử dụng ít bộ nhớ hơn, một chuỗi trống hay bool. Tôi đoán sẽ là bool.
TTT

Trong từ điển, một stringtham chiếu và một boolgiá trị tạo ra sự khác biệt 3 hoặc 7 byte, tương ứng với các hệ thống 32 hoặc 64 bit. Tuy nhiên, lưu ý rằng kích thước của mỗi mục nhập được làm tròn đến bội số của 4 hoặc 8, tương ứng. Do đó, sự lựa chọn giữa stringboolcó thể không tạo ra bất kỳ sự khác biệt nào về kích thước. Chuỗi trống ""luôn tồn tại trong bộ nhớ dưới dạng thuộc tính tĩnh string.Empty, vì vậy nó không tạo ra bất kỳ sự khác biệt nào cho dù bạn có sử dụng nó trong từ điển hay không. (Và dù sao nó cũng được sử dụng ở những nơi khác.)
Wormbo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.