HashSet <T> .removeAll पद्धति आश्चर्यजनक रूप से धीमी है


92

जॉन स्कीट ने हाल ही में अपने ब्लॉग पर एक दिलचस्प प्रोग्रामिंग विषय उठाया: "मेरे अमूर्त में एक छेद है, प्रिय लिजा, प्रिय लिजा" (जोर दिया):

मेरे पास एक सेट है - HashSetवास्तव में। मैं इसमें से कुछ वस्तुओं को निकालना चाहता हूं ... और बहुत से आइटम अच्छी तरह से मौजूद नहीं हो सकते हैं। वास्तव में, हमारे परीक्षण के मामले में, "रिमूवल" संग्रह में कोई भी वस्तु मूल सेट में नहीं होगी। यह लगता है - और वास्तव में है - कोड के लिए बेहद आसान है। आखिरकार, हमें Set<T>.removeAllमदद करने के लिए मिल गया है, है ना?

हम "स्रोत" सेट का आकार और कमांड लाइन पर "रिमूवल" संग्रह का आकार निर्दिष्ट करते हैं, और दोनों का निर्माण करते हैं। स्रोत सेट में केवल गैर-नकारात्मक पूर्णांक होते हैं; निष्कासन सेट में केवल नकारात्मक पूर्णांक होते हैं। हम मापते हैं कि उपयोग करने वाले सभी तत्वों को हटाने में कितना समय लगता है System.currentTimeMillis(), जो कि दुनिया का सबसे सटीक स्टॉपवॉच नहीं है, लेकिन इस मामले में पर्याप्त से अधिक है, जैसा कि आप देखेंगे। यहाँ कोड है:

import java.util.*;
public class Test 
{ 
    public static void main(String[] args) 
    { 
       int sourceSize = Integer.parseInt(args[0]); 
       int removalsSize = Integer.parseInt(args[1]); 
        
       Set<Integer> source = new HashSet<Integer>(); 
       Collection<Integer> removals = new ArrayList<Integer>(); 
        
       for (int i = 0; i < sourceSize; i++) 
       { 
           source.add(i); 
       } 
       for (int i = 1; i <= removalsSize; i++) 
       { 
           removals.add(-i); 
       } 
        
       long start = System.currentTimeMillis(); 
       source.removeAll(removals); 
       long end = System.currentTimeMillis(); 
       System.out.println("Time taken: " + (end - start) + "ms"); 
    }
}

चलो इसे एक आसान काम देकर शुरू करें: 100 वस्तुओं का स्रोत सेट, और हटाने के लिए 100:

c:UsersJonTest>java Test 100 100
Time taken: 1ms

ठीक है, इसलिए हमने उम्मीद नहीं की थी कि यह धीमा होगा ... स्पष्ट रूप से हम चीजों को थोड़ा ऊपर कर सकते हैं। एक लाख वस्तुओं और 300,000 वस्तुओं के स्रोत के बारे में कैसे निकालें?

c:UsersJonTest>java Test 1000000 300000
Time taken: 38ms

हम्म। यह अभी भी बहुत तेज लगता है। अब मुझे लगता है कि मैं थोड़ा क्रूर हो गया हूं, यह सब हटाने के लिए कह रहा हूं। चलो इसे थोड़ा आसान बनाते हैं - 300,000 स्रोत आइटम और 300,000 निष्कासन:

c:UsersJonTest>java Test 300000 300000
Time taken: 178131ms

क्षमा कीजिय? लगभग तीन मिनट ? ओह! निश्चित रूप से यह एक छोटे संग्रह से आइटम निकालने में आसान होना चाहिए जो हम 38ms में प्रबंधित करते हैं?

क्या कोई समझा सकता है कि ऐसा क्यों हो रहा है? HashSet<T>.removeAllविधि इतनी धीमी क्यों है ?


2
मैंने आपके कोड का परीक्षण किया और इसने तेजी से काम किया। आपके मामले में, इसे समाप्त होने में ~ 12ms का समय लगा। मैंने दोनों इनपुट मानों में 10 की वृद्धि की है और इसमें 36ms लगे हैं। हो सकता है कि परीक्षण चलाते समय आपका पीसी कुछ गहन सीपीयू कार्य करता हो?
स्लिमू

4
मैंने इसका परीक्षण किया, और ओपी (ठीक है, मैंने इसे अंत से पहले रोक दिया) के समान परिणाम था। वास्तव में अजीब। विंडोज, जेडीके 1.7.0_55
जेबी निज़ेट

2
इस पर एक खुला टिकट है: JDK-6982173
हाओहजुन

44
जैसा कि मेटा पर चर्चा की गई थी , यह सवाल मूल रूप से जॉन स्कीट के ब्लॉग (अब सीधे उद्धृत किया गया था और एक मॉडरेटर के संपादन के कारण प्रश्न से जुड़ा हुआ है) से साहित्यिक चोरी है। भविष्य के पाठकों को यह ध्यान देना चाहिए कि जिस ब्लॉग पोस्ट से यह किया गया था वह वास्तव में व्यवहार का कारण बताता है, इसी तरह यहाँ स्वीकृत उत्तर के लिए। जैसे, यहाँ जवाब पढ़ने के बजाय, आप इसके बजाय बस के माध्यम से क्लिक करना चाहते हैं और पूरा ब्लॉग पोस्ट पढ़ सकते हैं ।
मार्क अमेरी

1
बग को जावा 15: JDK-6394757
ZhekaKozlov

जवाबों:


138

व्यवहार (कुछ) javadoc में प्रलेखित है :

यह कार्यान्वयन निर्धारित करता है कि प्रत्येक पर आकार विधि को लागू करके इस सेट और निर्दिष्ट संग्रह में से कौन सा छोटा है। यदि इस सेट में कम तत्व हैं , तो कार्यान्वयन इस सेट पर पुनरावृत्त करता है, प्रत्येक तत्व को पुनरावृत्त द्वारा जाँच कर यह देखने के लिए कि क्या यह निर्दिष्ट संग्रह में निहित है । यदि यह बहुत सम्‍मिलित है, तो इसे इट्रेटर की निष्कासन विधि के साथ इस सेट से हटा दिया जाता है। यदि निर्दिष्ट संग्रह में कम तत्व हैं, तो कार्यान्वयन निर्दिष्ट संग्रह पर पुनरावृत्त करता है, इस सेट से हटाकर, इस सेट को हटाने की विधि का उपयोग करते हुए, इट्रेटर द्वारा लौटाए गए प्रत्येक तत्व को हटा देता है।

जब आप कॉल करते हैं तो इसका क्या अर्थ है source.removeAll(removals);:

  • यदि removalsसंग्रह इससे छोटे आकार का है source, तो removeविधि HashSetको कहा जाता है, जो तेज है।

  • यदि removalsसंग्रह समान या उससे बड़े आकार का है source, तो removals.containsउसे कहा जाता है, जो एक ArrayList के लिए धीमा है।

जल्दी ठीक:

Collection<Integer> removals = new HashSet<Integer>();

ध्यान दें कि एक खुला बग है जो आपके वर्णन के समान है। लब्बोलुआब यह है कि यह शायद एक गरीब विकल्प है, लेकिन इसे बदला नहीं जा सकता क्योंकि यह javadoc में प्रलेखित है।


संदर्भ के लिए, यह removeAll(जावा 8 में - अन्य संस्करणों की जाँच नहीं की गई) का कोड है:

public boolean removeAll(Collection<?> c) {
    Objects.requireNonNull(c);
    boolean modified = false;

    if (size() > c.size()) {
        for (Iterator<?> i = c.iterator(); i.hasNext(); )
            modified |= remove(i.next());
    } else {
        for (Iterator<?> i = iterator(); i.hasNext(); ) {
            if (c.contains(i.next())) {
                i.remove();
                modified = true;
            }
        }
    }
    return modified;
}

15
वाह। मैंने आज कुछ सीखा। यह मेरे लिए एक बुरी कार्यान्वयन पसंद की तरह दिखता है। उन्हें ऐसा नहीं करना चाहिए यदि अन्य संग्रह सेट नहीं है।
जेबी निज़ेट

2
@JBNizet हां यह अजीब है - यह आपके सुझाव के साथ यहां चर्चा की गई है - यकीन नहीं कि यह क्यों नहीं हुआ ...
assylias

2
बहुत बहुत धन्यवाद @assylias .. लेकिन वास्तव में सोच रहे थे कि आपने इसे कैसे निकाला .. :) बहुत अच्छा लगा .... क्या आपने इस समस्या का सामना किया ???

8
@show_stopper मैंने सिर्फ एक प्रोफाइलर चलाया और देखा कि ArrayList#containsवह अपराधी था। AbstractSet#removeAllशेष उत्तर दिए गए कोड पर एक नज़र ।
assylias
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.