รายการ <T> .Contains () ช้ามาก?


94

ใครช่วยอธิบายหน่อยได้ไหมว่าทำไมList.Contains()ฟังก์ชันgenerics ช้าจัง

ฉันมีList<long>ตัวเลขประมาณล้านตัวและรหัสที่คอยตรวจสอบอยู่ตลอดเวลาว่ามีตัวเลขเฉพาะในตัวเลขเหล่านี้หรือไม่

ฉันลองทำสิ่งเดียวกันโดยใช้Dictionary<long, byte>และDictionary.ContainsKey()ฟังก์ชั่นและมันเร็วกว่ารายการประมาณ 10-20 เท่า

แน่นอนว่าฉันไม่ต้องการใช้ Dictionary เพื่อจุดประสงค์นั้นจริงๆเพราะมันไม่ได้ตั้งใจให้ใช้แบบนั้น

ดังนั้นคำถามที่แท้จริงที่นี่คือมีทางเลือกอื่นนอกเหนือจากList<T>.Contains()นี้ แต่ไม่เลวร้ายเท่าDictionary<K,V>.ContainsKey()?


2
พจนานุกรมมีปัญหาอะไร มีไว้สำหรับใช้ในกรณีเช่นของคุณ
Kamarey

4
@Kamarey: HashSet อาจเป็นตัวเลือกที่ดีกว่า
Brian Rasmussen

HashSet คือสิ่งที่ฉันกำลังมองหา
DSent

คำตอบ:


160

หากคุณแค่ตรวจสอบว่ามีอยู่จริง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

รายการเนื้อหาเป็นการดำเนินการ O (n)

Dictionary.ContainsKey เป็นการดำเนินการ O (1) เนื่องจากใช้แฮชโค้ดของอ็อบเจ็กต์เป็นคีย์ซึ่งทำให้คุณสามารถค้นหาได้เร็วขึ้น

ฉันไม่คิดว่าเป็นความคิดที่ดีที่จะมีรายการที่มีรายการเป็นล้านรายการ ฉันไม่คิดว่าคลาส List ถูกออกแบบมาเพื่อจุดประสงค์นั้น :)

เป็นไปไม่ได้ที่จะบันทึกเอนทิตีระดับล้านเหล่านั้นลงใน RDBMS และดำเนินการสืบค้นบนฐานข้อมูลนั้นหรือไม่?

ถ้าเป็นไปไม่ได้ฉันก็จะใช้ Dictionary อยู่ดี


13
ฉันไม่คิดว่ามีอะไรที่ไม่เหมาะสมเกี่ยวกับรายการที่มีรายการเป็นล้านรายการ แต่คุณอาจไม่ต้องการเรียกใช้การค้นหาเชิงเส้นต่อไป
Will Dean

เห็นด้วยไม่มีอะไรผิดพลาดกับรายการหรืออาร์เรย์ที่มีหลายรายการ เพียงแค่ไม่สแกนหาค่า
Michael Krauklis

8

ฉันคิดว่าฉันมีคำตอบ! ใช่เป็นเรื่องจริงที่มี () ในรายการ (อาร์เรย์) คือ O (n) แต่ถ้าอาร์เรย์สั้นและคุณใช้ประเภทค่าก็ยังควรจะค่อนข้างเร็ว แต่เมื่อใช้ CLR Profiler [ดาวน์โหลดฟรีจาก Microsoft] ฉันพบว่ามี () เป็นค่ามวยเพื่อเปรียบเทียบซึ่งต้องมีการจัดสรรฮีปซึ่งมีราคาแพงมาก (ช้า) [หมายเหตุ: นี่คือ. Net 2.0; ไม่ได้ทดสอบ. Net เวอร์ชันอื่น ๆ ]

นี่คือเรื่องราวและวิธีแก้ปัญหาทั้งหมด เรามีการแจงนับที่เรียกว่า "VI" และสร้างคลาสที่เรียกว่า "ValueIdList" ซึ่งเป็นประเภทนามธรรมสำหรับรายการ (อาร์เรย์) ของวัตถุ VI การใช้งานดั้งเดิมอยู่ใน. Net 1.1 วันโบราณและใช้ ArrayList แบบห่อหุ้ม เราค้นพบเมื่อเร็ว ๆ นี้ในhttp://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspxว่ารายการทั่วไป (List <VI>) ทำงานได้ดีกว่า ArrayList ในประเภทค่า (เช่นของเรา enum VI) เนื่องจากค่าไม่จำเป็นต้องอยู่ในกล่อง เป็นเรื่องจริงและได้ผล ... เกือบ

CLR Profiler เผยเซอร์ไพรส์ นี่คือส่วนหนึ่งของกราฟการจัดสรร:

  • ValueIdList :: มีบูล (VI) 5.5MB (34.81%)
  • Generic.List :: มีบูล (<UNKNOWN>) 5.5MB (34.81%)
  • Generic.ObjectEqualityComparer <T> :: Equals bool (<UNKNOWN> <UNKNOWN>) 5.5MB (34.88%)
  • ค่า VI 7.7MB (49.03%)

อย่างที่คุณเห็นมี () เรียก Generic.ObjectEqualityComparer.Equals () อย่างน่าประหลาดใจซึ่งเห็นได้ชัดว่าต้องใช้ค่า VI ซึ่งต้องการการจัดสรรฮีปที่มีราคาแพง เป็นเรื่องแปลกที่ Microsoft จะกำจัดการชกมวยในรายการเพียงเพื่อต้องการอีกครั้งสำหรับการใช้งานที่เรียบง่ายเช่นนี้

วิธีแก้ปัญหาของเราคือเขียนการใช้งานประกอบด้วย () อีกครั้งซึ่งในกรณีของเราทำได้ง่ายเนื่องจากเราได้ห่อหุ้มวัตถุรายการทั่วไป (_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 ซึ่งอนุญาตให้ใช้เฉพาะองค์ประกอบที่ไม่ซ้ำกันเท่านั้น


พจนานุกรม <Type, integer> สามารถจัดเก็บออบเจ็กต์ที่ไม่ซ้ำกันได้อย่างมีประสิทธิภาพเช่นกัน - ใช้จำนวนเต็มเพื่อนับจำนวนรายการที่ซ้ำกัน ตัวอย่างเช่นคุณจะจัดเก็บรายการ {a, b, a} เป็น {a = 2, b = 1} แน่นอนมันจะสูญเสียการบวช
MSalters


2

นี่ไม่ใช่คำตอบสำหรับคำถามของคุณ แต่ฉันมีคลาสที่เพิ่มประสิทธิภาพของมี () ในคอลเล็กชัน ฉันซับคลาสคิวและเพิ่มพจนานุกรมที่แมปรหัสแฮชกับรายการวัตถุ Dictionary.Contains()ฟังก์ชั่นคือ O (1) ในขณะที่List.Contains(), Queue.Contains()และStack.Contains()เป็น O (n)

ประเภทค่าของพจนานุกรมคือคิวที่ถืออ็อบเจ็กต์ที่มีแฮชโค้ดเดียวกัน ผู้เรียกสามารถจัดหาคลาสอ็อบเจ็กต์แบบกำหนดเองที่ใช้ IEqualityComparer คุณสามารถใช้รูปแบบนี้สำหรับ Stacks หรือ Lists รหัสจะต้องมีการเปลี่ยนแปลงเพียงเล็กน้อย

/// <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 ใช้เวลา 0.00045 วินาทีสำหรับเวอร์ชัน HashQueue และ 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();
}

ฉันเพิ่งเพิ่มกรณีทดสอบที่ 3 สำหรับ HashSet <T> ซึ่งดูเหมือนว่าจะได้ผลลัพธ์ที่ดีกว่าโซลูชันของคุณ: HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek

1

เหตุใดพจนานุกรมจึงไม่เหมาะสม

หากต้องการดูว่ามีค่าใดอยู่ในรายการหรือไม่คุณต้องเดินทั้งรายการ ด้วยพจนานุกรม (หรือคอนเทนเนอร์ที่ใช้แฮชอื่น ๆ ) การ จำกัด จำนวนวัตถุที่คุณต้องการเปรียบเทียบให้แคบลงได้เร็วกว่ามาก คีย์ (ในกรณีของคุณคือตัวเลข) ถูกแฮชและทำให้พจนานุกรมมีส่วนย่อยเศษส่วนของวัตถุเพื่อเปรียบเทียบกับ


0

ฉันใช้สิ่งนี้ใน Compact Framework ซึ่งไม่มีการรองรับ HashSet ฉันได้เลือกใช้พจนานุกรมที่ทั้งสองสตริงเป็นค่าที่ฉันกำลังมองหา

หมายความว่าฉันได้รับฟังก์ชั่นรายการ <> พร้อมประสิทธิภาพพจนานุกรม มันค่อนข้างแฮ็ค แต่ใช้งานได้


1
หากคุณใช้พจนานุกรมแทน HashSet คุณอาจตั้งค่าเป็น "" แทนที่จะใช้สตริงเดียวกับคีย์ ด้วยวิธีนี้คุณจะใช้หน่วยความจำน้อยลง หรือคุณสามารถใช้ Dictionary <string, bool> และตั้งค่าทั้งหมดเป็นจริง (หรือเท็จ) ฉันไม่รู้ว่าจะใช้หน่วยความจำน้อยกว่าสตริงว่างหรือบูล ฉันเดาว่าน่าจะเป็นบูล
ท.ท.

ในพจนานุกรมการstringอ้างอิงและboolค่าสร้างความแตกต่างเป็น 3 หรือ 7 ไบต์สำหรับระบบ 32 หรือ 64 บิตตามลำดับ อย่างไรก็ตามโปรดทราบว่าขนาดของแต่ละรายการจะถูกปัดขึ้นเป็นทวีคูณของ 4 หรือ 8 ตามลำดับ ทางเลือกระหว่างstringและboolจึงอาจไม่ทำให้ขนาดแตกต่างกันเลย สตริงว่าง""จะมีอยู่ในหน่วยความจำเสมอเป็นคุณสมบัติคงที่string.Emptyดังนั้นจึงไม่สร้างความแตกต่างใด ๆ ไม่ว่าคุณจะใช้ในพจนานุกรมหรือไม่ก็ตาม (และถูกนำไปใช้ที่อื่นด้วย)
Wormbo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.