হ্যাশসেট <টি>। সমস্ত সরানো পদ্ধতি আশ্চর্যজনকভাবে ধীর


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পদ্ধতিটি এত ধীর কেন ?


4
আমি আপনার কোডটি পরীক্ষা করেছি এবং এটি দ্রুত কাজ করেছে। আপনার ক্ষেত্রে, এটি শেষ হতে 12 মিলিয়ন ডলার নিয়েছিল। আমি উভয় ইনপুট মান 10 দ্বারা বাড়িয়েছি এবং এটি 36 মিনিট নিয়েছে। আপনি পিসি চালানোর সময় আপনার পিসি কিছু নিবিড় সিপিইউ কাজ করে?
স্লিমু

4
আমি এটি পরীক্ষা করেছি এবং ওপি-র মতো একই ফলাফল পেয়েছি (ভাল, আমি এটি শেষ হওয়ার আগেই থামিয়ে দিয়েছিলাম)। অদ্ভুত প্রকৃতপক্ষে. উইন্ডোজ, জেডিকে 1.7.0_55
জেবি নিজেট


44
মেটাতে আলোচিত হিসাবে , এই প্রশ্নটি মূলত জন স্কিটির ব্লগ থেকে চুরি করা হয়েছিল (এখন একজন সংস্থার সম্পাদনার কারণে সরাসরি প্রশ্ন থেকে উদ্ধৃত এবং প্রশ্নের সাথে যুক্ত)। ভবিষ্যতের পাঠকদের মনে রাখা উচিত যে এটি যে ব্লগ পোস্ট থেকে চুরি হয়েছিল তা আসলে আচরণের কারণটি এখানে গ্রহণযোগ্য উত্তরের অনুরূপ ব্যাখ্যা করে। যেমন, এখানে উত্তরগুলি পড়ার পরিবর্তে আপনি কেবলমাত্র সম্পূর্ণ ব্লগ পোস্টের মাধ্যমে ক্লিক করতে এবং পড়তে ইচ্ছুক হতে পারেন ।
মার্ক অ্যামেরি

4
বাগটি জাভা 15 এ স্থির করা হবে: জেডিকে-6394757
he

উত্তর:


139

আচরণটি (কিছুটা) জাভাদকে নথিভুক্ত করা হয়েছে :

এই প্রয়োগটি প্রতিটিটিতে আকারের পদ্ধতিতে অনুরোধ করে এই সেট এবং নির্দিষ্ট সংগ্রহের চেয়ে ছোটটি নির্ধারণ করে। এই সেট কম উপাদান থাকে , এই সেট উপর তারপর বাস্তবায়ন iterates, প্রতিটি উপাদান ঘুরে পুনরুক্তিকারীর দ্বারা ফিরে চেক করে দেখতে হলে নিদিষ্ট সংগ্রহে মধ্যে অন্তর্ভুক্ত করা হয় । যদি এটি এতটা থাকে তবে এটি সেটরটি পুনরাবৃত্তির অপসারণ পদ্ধতির সাহায্যে সরানো হবে। যদি নির্দিষ্ট সংগ্রহটিতে কম উপাদান থাকে, তবে প্রয়োগটি নির্দিষ্ট সংগ্রহের উপরে পুনরাবৃত্তি করে, এই সেট থেকে অপসারণকারী প্রতিটি উপাদান এই সেটটির অপসারণ পদ্ধতিটি ব্যবহার করে পুনরায় সেট করে removing

বাস্তবে এর অর্থ কী, যখন আপনি কল করবেন source.removeAll(removals);:

  • যদি removalsসংগ্রহে চেয়ে ছোট সাইজ হয় source, removeপদ্ধতি HashSetবলা হয়, যা দ্রুত।

  • যদি removalsসংগ্রহটি সমান বা বৃহত্তর আকারের হয় sourceতবে removals.containsতাকে বলা হয়, যা অ্যারেলিস্টের জন্য ধীর।

দ্রুত ঠিক করা:

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

মনে রাখবেন যে এখানে একটি ওপেন বাগ রয়েছে যা আপনি বর্ণনা করেছেন তার সাথে খুব মিল। নীচের অংশটি মনে হচ্ছে এটি সম্ভবত একটি দুর্বল পছন্দ তবে এটি পরিবর্তন করা যাচ্ছে না কারণ এটি জাভাদোকে নথিভুক্ত রয়েছে।


রেফারেন্সের জন্য, এটি কোড 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
কি দারুন. আমি আজ কিছু শিখেছি। এটি আমার কাছে খারাপ প্রয়োগের পছন্দ বলে মনে হচ্ছে। অন্য সংগ্রহটি সেট না হলে তাদের তা করা উচিত নয়।
জেবি নিজত

4
@ জেবিনিজেট হ্যাঁ এটি অদ্ভুত - এটি আপনার পরামর্শ নিয়ে এখানে আলোচনা করা হয়েছে - কেন এটি
অতিক্রান্ত

4
অনেক ধন্যবাদ @ দূতাবাস .. তবে সত্যিই ভাবছেন আপনি কীভাবে এটি আবিষ্কার করলেন .. :) সত্যিই খুব ভাল লাগছে .... আপনি কি এই সমস্যার মুখোমুখি হয়েছিলেন ???

8
@ শো_স্টোপার আমি কেবল একজন প্রোফাইলার চালাচ্ছি এবং দেখলাম যে ArrayList#containsএটিই অপরাধী। কোডটি একবার দেখে AbstractSet#removeAllউত্তরটি দিয়েছিল।
assylias
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.