हैशसेट कोड के लिए अनपेक्षित समय


28

तो मूल रूप से, मेरे पास यह कोड था:

import java.util.*;

public class sandbox {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 100_000; i++) {
            hashSet.add(i);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100_000; i++) {
            for (Integer val : hashSet) {
                if (val != -1) break;
            }

            hashSet.remove(i);
        }

        System.out.println("time: " + (System.currentTimeMillis() - start));
    }
}

यह मेरे कंप्यूटर पर छोरों के लिए नेस्टेड को चलाने के लिए 4 जी के चारों ओर ले जाता है और मुझे समझ में नहीं आता है कि इसमें इतना समय क्यों लगा। बाहरी लूप 100,000 बार चलता है, लूप के लिए आंतरिक 1 बार चलना चाहिए (क्योंकि हैशसेट का कोई भी मूल्य -1 नहीं होगा) और एक हैशेट से एक आइटम को हटाने का ओ (1) है, इसलिए लगभग 200,000 ऑपरेशन होने चाहिए। अगर एक सेकंड में आम तौर पर 100,000,000 ऑपरेशन होते हैं, तो मेरा कोड आने के लिए 4s को कैसे लिया जाता है?

इसके अतिरिक्त, यदि लाइन hashSet.remove(i);पर टिप्पणी की जाती है, तो कोड केवल 16ms का होता है। यदि लूप के लिए आंतरिक पर टिप्पणी की जाती है (लेकिन नहीं hashSet.remove(i);), कोड केवल 8ms लेता है।


4
मैं आपके निष्कर्षों की पुष्टि करता हूं। मैं कारण के बारे में अनुमान लगा सकता था, लेकिन उम्मीद है कि कोई चतुर व्यक्ति एक आकर्षक विवरण पोस्ट करेगा।
khelwood

1
ऐसा लगता है कि for valलूप समय लेने वाली चीज है। removeअभी भी बहुत तेजी से है। सेट को संशोधित किए जाने के बाद किसी प्रकार का ओवरहेड एक नया पुनरावृत्ति सेट करने वाला ...?
khelwood

क्यों लूप धीमा है , इसके लिए @apangin ने stackoverflow.com/a/59522575/108326 में एक अच्छा विवरण दिया for val। हालाँकि, ध्यान दें कि लूप की बिल्कुल ज़रूरत नहीं है। यदि आप यह जांचना चाहते हैं कि क्या सेट में -1 से भिन्न कोई मान हैं, तो यह जांचना अधिक कुशल होगा hashSet.size() > 1 || !hashSet.contains(-1)
अंकुश

जवाबों:


32

आपने एक सीमांत उपयोग का मामला बनाया है HashSet, जहाँ एल्गोरिथ्म द्विघात जटिलता को कम करता है।

यहाँ सरलीकृत लूप है जो इतना समय लेता है:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}

async-profiler दिखाता है कि लगभग सभी समय java.util.HashMap$HashIterator()कंस्ट्रक्टर के अंदर बिताया जाता है :

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
--->        do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

हाइलाइट की गई रेखा एक रैखिक लूप है जो हैश तालिका में पहले गैर-खाली बाल्टी की खोज करती है।

चूंकि Integerट्रिवियल hashCode(यानी हैशकोड संख्या के बराबर है), यह पता चला है कि लगातार पूर्णांक ज्यादातर हैश तालिका में लगातार बाल्टी पर कब्जा कर लेते हैं: नंबर 0 पहली बाल्टी में जाता है, नंबर 1 दूसरी बाल्टी में जाता है, आदि।

अब आप लगातार संख्याओं को 0 से 99999 तक हटा देते हैं। सरलतम स्थिति में (जब बाल्टी में एकल कुंजी होती है) कुंजी को हटाने को बाल्टी सरणी में संबंधित तत्व को शून्य करने के रूप में लागू किया जाता है। ध्यान दें कि हटाने के बाद तालिका को संकुचित या पुन: व्यवस्थित नहीं किया गया है।

तो, बकेट ऐरे की शुरुआत से आप जितनी अधिक चाबियाँ निकालते HashIteratorहैं, पहले गैर-खाली बकेट को खोजने के लिए लंबे समय की आवश्यकता होती है।

कुंजी को दूसरे छोर से हटाने का प्रयास करें:

hashSet.remove(100_000 - i);

एल्गोरिथ्म नाटकीय रूप से तेज हो जाएगा!


1
आह, मैं इस पर आया था, लेकिन पहले कुछ रन के बाद इसे खारिज कर दिया और सोचा कि यह कुछ JIT अनुकूलन हो सकता है और JITWatch के माध्यम से विश्लेषण करने के लिए स्थानांतरित हो गया। पहले async-profiler चलाना चाहिए था। अरे नहीं!
अद्वैत कुमार

1
काफी मनोरंजक। यदि आप लूप में निम्नलिखित की तरह कुछ करते हैं, तो यह आंतरिक मानचित्र के आकार को कम करके इसे गति देता है if (i % 800 == 0) { hashSet = new HashSet<>(hashSet); }:।
ग्रे - SO
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.