วิธี HashSet <T> .removeAll นั้นช้าอย่างน่าตกใจ


92

เมื่อเร็ว ๆ นี้ Jon Skeet ได้ยกหัวข้อการเขียนโปรแกรมที่น่าสนใจในบล็อกของเขา: "มีช่องว่างในสิ่งที่เป็นนามธรรมของฉัน Liza ที่รัก Liza ที่รัก" (เน้นเพิ่มเติม):

ฉันมีชุด - อันที่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 และใช้เวลา 36 มิลลิวินาที บางทีพีซีของคุณอาจทำงาน CPU อย่างเข้มข้นในขณะที่คุณทำการทดสอบ?
Slimu

4
ฉันทดสอบแล้วและได้ผลลัพธ์เช่นเดียวกับ OP (ฉันหยุดมันก่อนที่จะสิ้นสุด) แปลกจริงๆ Windows, JDK 1.7.0_55
JB Nizet

2
มีตั๋วเปิดอยู่ที่: JDK-6982173
Haozhun

44
ตามที่กล่าวไว้ใน Metaคำถามนี้ถูกลอกเลียนแบบมาจากบล็อกของ Jon Skeet (ตอนนี้อ้างถึงโดยตรงและเชื่อมโยงกับคำถามเนื่องจากการแก้ไขของผู้ดูแล) ผู้อ่านในอนาคตควรทราบว่าบล็อกโพสต์นั้นลอกเลียนแบบมาจากไม่ได้อธิบายถึงสาเหตุของพฤติกรรมในทำนองเดียวกันกับคำตอบที่ยอมรับที่นี่ ดังนั้นแทนที่จะอ่านคำตอบที่นี่คุณอาจต้องการเพียงแค่คลิกผ่านและอ่านบล็อกโพสต์ทั้งหมด
Mark Amery

1
ข้อบกพร่องจะได้รับการแก้ไขใน Java 15: JDK-6394757
ZhekaKozlov

คำตอบ:


139

พฤติกรรม (ค่อนข้าง) ที่บันทึกไว้ในjavadoc :

การใช้งานนี้กำหนดว่าชุดใดเล็กกว่าของชุดนี้และคอลเล็กชันที่ระบุโดยการเรียกใช้เมธอดขนาดในแต่ละชุด หากชุดนี้มีองค์ประกอบน้อยกว่าการใช้งานจะวนซ้ำชุดนี้ตรวจสอบแต่ละองค์ประกอบที่ส่งคืนโดยตัววนซ้ำเพื่อดูว่ามีอยู่ในคอลเล็กชันที่ระบุหรือไม่ หากมีอยู่ชุดนี้จะถูกลบออกจากชุดนี้ด้วยวิธีการลบของตัววนซ้ำ หากคอลเล็กชันที่ระบุมีองค์ประกอบน้อยกว่าการใช้งานจะวนซ้ำบนคอลเล็กชันที่ระบุโดยการลบชุดนี้แต่ละองค์ประกอบที่ส่งคืนโดยตัววนซ้ำโดยใช้วิธีการลบของชุดนี้

ในทางปฏิบัติหมายความว่าอย่างไรเมื่อคุณโทรsource.removeAll(removals);:

  • ถ้าremovalsคอลเลกชันที่มีขนาดเล็กกว่าsourceที่removeวิธีการHashSetที่เรียกว่าซึ่งเป็นไปอย่างรวดเร็ว

  • ถ้าremovalsคอลเลกชันที่มีขนาดเท่ากันหรือใหญ่กว่าsourceนั้นremovals.containsจะเรียกว่าที่ช้าสำหรับ ArrayList

แก้ไขด่วน:

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

โปรดทราบว่ามีข้อบกพร่องแบบเปิดที่คล้ายกับที่คุณอธิบายไว้มาก บรรทัดล่างดูเหมือนว่าอาจเป็นทางเลือกที่ไม่ดี แต่ไม่สามารถเปลี่ยนแปลงได้เนื่องจากมีการบันทึกไว้ใน javadoc


สำหรับการอ้างอิงนี่คือรหัสของremoveAll(ใน Java 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
ว้าว. ฉันได้เรียนรู้บางสิ่งในวันนี้ นี่ดูเหมือนเป็นการเลือกใช้งานที่ไม่ดีสำหรับฉัน พวกเขาไม่ควรทำเช่นนั้นหากคอลเลกชันอื่นไม่ใช่ชุด
JB Nizet

2
@JBNizet ใช่มันแปลก - มีการพูดคุยกับคำแนะนำของคุณที่นี่ - ไม่แน่ใจว่าทำไมมันถึงไม่ผ่าน ...
assylias

2
ขอบคุณมาก @assylias .. แต่สงสัยจริงๆว่าคุณคิดออกได้อย่างไร .. :) ดีจริงๆ .... คุณประสบปัญหานี้หรือไม่ ???

8
@show_stopper ฉันเพิ่งทำงานโปรไฟล์และเห็นว่านั่นArrayList#containsเป็นตัวการ ดูรหัสของAbstractSet#removeAllให้คำตอบที่เหลือ
assylias
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.