सूची <T>। कोनटेनेंस () बहुत धीमा है?


93

क्या कोई मुझे समझा सकता है कि जेनेरिक List.Contains()फ़ंक्शन इतना धीमा क्यों है?

मेरे पास List<long>लगभग एक लाख संख्याएँ हैं, और कोड जो लगातार जाँच रहा है कि क्या इन संख्याओं के भीतर कोई विशिष्ट संख्या है।

मैंने एक ही चीज़ का उपयोग करने Dictionary<long, byte>और Dictionary.ContainsKey()फ़ंक्शन करने की कोशिश की , और यह सूची के साथ लगभग 10-20 गुना तेज था।

बेशक, मैं वास्तव में उस उद्देश्य के लिए डिक्शनरी का उपयोग नहीं करना चाहता, क्योंकि इसका उस तरह से उपयोग करने का मतलब नहीं था।

तो, यहाँ असली सवाल यह है कि क्या कोई विकल्प List<T>.Contains()नहीं है, लेकिन उतना अजीब नहीं है Dictionary<K,V>.ContainsKey()?


2
शब्दकोश के साथ क्या समस्या है? यह तुम्हारे जैसे मामले में उपयोग के लिए है।
कमारेय

4
@Kamarey: हैशसेट एक बेहतर विकल्प हो सकता है।
ब्रायन रासमुसेन

हैशसेट वही है जिसकी मुझे तलाश थी।
प्रातः

जवाबों:


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) ऑपरेशन है, क्योंकि यह एक कुंजी के रूप में वस्तुओं के हैशकोड का उपयोग करता है, जो आपको एक त्वरित खोज क्षमता देता है।

मुझे नहीं लगता है कि यह एक अच्छा विचार है एक सूची है जिसमें एक लाख प्रविष्टियाँ हैं। मुझे नहीं लगता कि सूची वर्ग उस उद्देश्य के लिए डिज़ाइन किया गया था। :)

उदाहरण के लिए, उन मिलन संस्थाओं को RDBMS में सहेजना संभव नहीं है, और उस डेटाबेस पर प्रश्न करना है?

यदि यह संभव नहीं है, तो मैं वैसे भी एक शब्दकोश का उपयोग करूंगा।


13
मुझे नहीं लगता कि एक लाख वस्तुओं के साथ एक सूची के बारे में कुछ भी अनुचित है, यह सिर्फ इतना है कि आप शायद इसके पार रैखिक खोज नहीं रखना चाहते हैं।
विल डीन

सहमत, एक सूची के साथ कुछ भी गलत नहीं है और न ही कई प्रविष्टियों के साथ एक सरणी। बस मूल्यों के लिए स्कैन न करें।
माइकल क्रुकलिस

8

मुझे लगता है कि मेरे पास इसका जवाब है! हां, यह सही है कि किसी सूची (सरणी) पर Contains () O (n) है, लेकिन यदि सरणी कम है और आप मान प्रकार का उपयोग कर रहे हैं, तो यह अभी भी काफी तेज होना चाहिए। लेकिन CLR Profiler [Microsoft से मुफ्त डाउनलोड] का उपयोग करते हुए, मैंने पाया कि उनमें तुलना करने के लिए Contains () बॉक्सिंग मूल्य है, जिसके लिए ढेर आवंटन की आवश्यकता है, जो बहुत महंगा (धीमा) है। [नोट: यह .Net 2.0 है; अन्य .Net संस्करणों का परीक्षण नहीं किया गया है।]

यहाँ पूरी कहानी और समाधान है। हमारे पास "VI" नाम की एक कल्पना है और "ValueIdList" नामक एक वर्ग बनाया है जो VI ऑब्जेक्ट्स की सूची (सरणी) के लिए एक अमूर्त प्रकार है। मूल कार्यान्वयन प्राचीन .net 1.1 दिनों में था, और इसने एक संक्षिप्त ArrayList का उपयोग किया था। हमने हाल ही में http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx में पाया कि एक सामान्य सूची (सूची <VI>) मूल्य प्रकारों पर ArrayList की तुलना में बहुत बेहतर प्रदर्शन करती है (जैसे हमारे enum VI) क्योंकि मूल्यों को बॉक्सिंग नहीं करना है। यह सच है और यह काम कर रहा है ... लगभग।

सीएलआर प्रोफाइलर ने एक आश्चर्य प्रकट किया। यहाँ आवंटन ग्राफ का एक भाग है:

  • ValueIdList :: इसमें बूल (VI) 5.5MB (34.81%) है
  • Generic.List :: इसमें बूल (<UNKNOWN>) 5.5MB (34.81%) शामिल है
  • Generic.ObjectEqualityComparer <T> :: बराबर बूल (<UNKNOWN> <UNKNOWN>) 5.5MB (34.88%)
  • मान .VI 7.7MB (49.03%)

जैसा कि आप देख सकते हैं, Contains () आश्चर्यजनक रूप से 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% तक बढ़ गया। ओ (n) ... कोई बात नहीं! बस व्यर्थ स्मृति उपयोग से बचें!


टिप के लिए धन्यवाद, मैं खुद खराब प्रदर्शन के कारण पकड़ा जा रहा था। एक कस्टम Containsकार्यान्वयन मेरे उपयोग के मामले के लिए तेज़ तरीका है।
ली हेस

5

शब्दकोश इतना बुरा नहीं है, क्योंकि एक शब्दकोश में चाबियाँ तेजी से पाए जाने के लिए डिज़ाइन की गई हैं। एक सूची में एक संख्या खोजने के लिए इसे पूरी सूची के माध्यम से पुनरावृत्त करना होगा।

बेशक शब्दकोश केवल तभी काम करता है जब आपके नंबर अद्वितीय हों और ऑर्डर न किए गए हों।

मुझे लगता है कि HashSet<T>.NET 3.5 में एक वर्ग भी है , यह केवल अद्वितीय तत्वों को भी अनुमति देता है।


एक शब्दकोश <प्रकार, पूर्णांक> प्रभावी ढंग से गैर-अद्वितीय वस्तुओं को भी संग्रहीत कर सकता है - डुप्लिकेट की संख्या की गणना करने के लिए पूर्णांक का उपयोग करें। उदाहरण के लिए, आप {a, b, a} को {a = 2, b = 1} सूची में संग्रहीत करेंगे। यह बेशक खो देता है।
MSalters 5'09


2

यह वास्तव में आपके प्रश्न का उत्तर नहीं है, लेकिन मेरे पास एक वर्ग है जो एक संग्रह पर सामग्री () को बढ़ाता है। मैंने एक कतार को उप-वर्गित किया और एक ऐसा शब्दकोश जोड़ा, जो वस्तुओं की सूचियों के लिए हैशकोड को मैप करता है। Dictionary.Contains()समारोह है हे (1) जबकि List.Contains(), Queue.Contains()और Stack.Contains()हे (एन) कर रहे हैं।

शब्दकोश का मूल्य-प्रकार समान हैशकोड के साथ वस्तुओं को पकड़ने वाली एक कतार है। कॉल करने वाला कस्टम क्लास ऑब्जेक्ट की आपूर्ति कर सकता है जो 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 से काउंट सेट के साथ टेस्ट कोड चलाने में हैशक्यू संस्करण के लिए 0.00045 सेकंड और क्यू संस्करण के लिए 0.37 सेकंड लगते हैं। 100,000 की गिनती के साथ, हाशिक्यू संस्करण 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 <टी> जो आपके समाधान की तुलना में और भी बेहतर परिणाम प्राप्त करने के लिए लगता है के लिए 3 परीक्षण का मामला जोड़ा HashQueue, 00:00:00.0004029 Queue, 00:00:00.3901439 HashSet, 00:00:00.0001716
psulek

1

एक शब्दकोश अनुचित क्यों है?

यह देखने के लिए कि सूची में कोई विशेष मूल्य है या नहीं, आपको पूरी सूची चलने की आवश्यकता है। शब्दकोश (या अन्य हैश आधारित कंटेनर) के साथ, यह उन वस्तुओं की संख्या को कम करने के लिए बहुत तेज है, जिनके खिलाफ आपको तुलना करने की आवश्यकता है। कुंजी (आपके मामले में, संख्या) हैशेड है और जो शब्दकोश को वस्तुओं की तुलना करने के लिए आंशिक उप-भाग देता है।


0

मैं इसका उपयोग कॉम्पैक्ट फ्रेमवर्क में कर रहा हूँ जहाँ हैशसेट के लिए कोई सपोर्ट नहीं है, मैंने एक ऐसे डिक्शनरी का विकल्प चुना है जहाँ दोनों स्ट्रिंग्स उस वैल्यू के लिए हैं जिसकी मुझे तलाश है।

इसका मतलब है कि मुझे शब्दकोश प्रदर्शन के साथ सूची <> कार्यक्षमता मिलती है। यह थोड़ा हैकी है, लेकिन यह काम करता है।


1
यदि आप किसी HashSet के बदले में डिक्शनरी का उपयोग कर रहे हैं, तो आप कुंजी के समान स्ट्रिंग के बजाय "" मान को सेट कर सकते हैं। इस तरह आप कम मेमोरी का उपयोग करेंगे। वैकल्पिक रूप से आप डिक्शनरी <string, bool> का भी उपयोग कर सकते हैं और उन सभी को सही (या गलत) पर सेट कर सकते हैं। मैं नहीं जानता कि कौन सी मेमोरी कम खाली स्ट्रिंग या बूल का उपयोग करेगी। मेरा अनुमान बूल होगा।
TTT

शब्दकोश में, एक stringसंदर्भ और boolमूल्य क्रमशः ३२ या ६४ बिट सिस्टम के लिए ३ या, बाइट्स का अंतर बनाते हैं। हालाँकि, ध्यान दें कि प्रत्येक प्रविष्टि का आकार क्रमशः 4 या 8 के गुणकों तक होता है। इस प्रकार चुनाव stringऔर boolइसके आकार में कोई अंतर नहीं हो सकता है। खाली स्ट्रिंग ""हमेशा स्मृति में पहले से ही स्थिर संपत्ति के रूप में मौजूद होती है string.Empty, इसलिए इससे कोई फर्क नहीं पड़ता है कि आप इसका उपयोग शब्दकोश में करते हैं या नहीं। (और वैसे भी इसका इस्तेमाल कहीं और किया जाता है।)
वॉर्मबो
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.