Metode HashSet <T> .removeAll ternyata sangat lambat

92

Jon Skeet baru-baru ini mengangkat topik pemrograman yang menarik di blognya: "Ada lubang dalam abstraksi saya, Liza sayang, Liza tersayang" (penekanan ditambahkan):

Saya punya satu set - a HashSet, sebenarnya. Saya ingin menghapus beberapa item darinya… dan banyak dari item tersebut mungkin tidak ada. Faktanya, dalam kasus pengujian kami, tidak ada item dalam koleksi "penghapusan" yang akan berada di set asli. Suara ini - dan memang adalah - sangat mudah untuk kode. Bagaimanapun, kita harus Set<T>.removeAllmembantu kita, bukan?

Kami menentukan ukuran kumpulan "sumber" dan ukuran kumpulan "penghapusan" pada baris perintah, dan membangun keduanya. Set sumber hanya berisi bilangan bulat non-negatif; kumpulan penghapusan hanya berisi bilangan bulat negatif. Kami mengukur berapa lama waktu yang dibutuhkan untuk menghapus semua elemen menggunakan System.currentTimeMillis(), yang bukan stopwatch paling akurat di dunia tetapi lebih dari cukup dalam kasus ini, seperti yang akan Anda lihat. Berikut kodenya:

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"); 
    }
}

Mari kita mulai dengan memberikan pekerjaan mudah: kumpulan sumber 100 item, dan 100 untuk dihapus:

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

Oke, jadi kami tidak menyangka ini akan lambat… jelas kami bisa sedikit meningkatkan. Bagaimana dengan sumber satu juta item dan 300.000 item untuk dihapus?

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

Hmm. Tampaknya masih cukup cepat. Sekarang saya merasa saya sedikit kejam, memintanya untuk melakukan semua itu. Mari membuatnya sedikit lebih mudah - 300.000 item sumber dan 300.000 penghapusan:

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

Permisi? Hampir tiga menit ? Astaga! Tentunya akan lebih mudah untuk menghapus item dari koleksi yang lebih kecil daripada yang kami kelola dalam 38ms?

Adakah yang bisa menjelaskan mengapa ini terjadi? Mengapa HashSet<T>.removeAllmetodenya sangat lambat?

Komunitas
sumber
2
Saya menguji kode Anda dan bekerja dengan cepat. Untuk kasus Anda, butuh ~ 12 md untuk selesai. Saya juga meningkatkan kedua nilai input sebesar 10 dan butuh 36ms. Mungkin PC Anda melakukan beberapa tugas CPU intensif saat Anda menjalankan tes?
Slimu
4
Saya mengujinya, dan mendapatkan hasil yang sama dengan OP (yah, saya menghentikannya sebelum akhir). Memang aneh. Windows, JDK 1.7.0_55
JB Nizet
2
Ada tiket terbuka untuk ini: JDK-6982173
Haozhun
44
Seperti yang dibahas di Meta , pertanyaan ini awalnya dijiplak dari blog Jon Skeet (sekarang dikutip langsung dari dan ditautkan ke dalam pertanyaan, karena suntingan moderator). Pembaca selanjutnya harus memperhatikan bahwa postingan blog tempat itu dijiplak sebenarnya menjelaskan penyebab perilaku tersebut, serupa dengan jawaban yang diterima di sini. Karena itu, daripada membaca jawaban di sini, Anda mungkin ingin mengklik dan membaca postingan blog selengkapnya .
Mark Amery
1
Bug akan diperbaiki di Java 15: JDK-6394757
ZhekaKozlov

Jawaban:

139

Perilaku tersebut (agak) didokumentasikan di javadoc :

Implementasi ini menentukan mana yang lebih kecil dari set ini dan koleksi yang ditentukan, dengan memanggil metode ukuran pada masing-masing. Jika set ini memiliki lebih sedikit elemen , maka implementasi mengulang set ini, memeriksa setiap elemen yang dikembalikan oleh iterator secara bergiliran untuk melihat apakah itu terdapat dalam koleksi yang ditentukan . Jika isinya begitu, maka akan dihapus dari set ini dengan metode remove iterator. Jika koleksi yang ditentukan memiliki lebih sedikit elemen, implementasi mengulangi koleksi yang ditentukan, menghapus dari set ini setiap elemen yang dikembalikan oleh iterator, menggunakan metode remove set ini.

Artinya dalam praktiknya, saat Anda menelepon source.removeAll(removals);:

  • jika removalskoleksi berukuran lebih kecil dari source, removemetode dari HashSetdisebut, yaitu cepat.

  • jika removalskoleksi berukuran sama atau lebih besar dari source, maka removals.containsdisebut, yang lambat untuk ArrayList.

Perbaikan cepat:

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

Perhatikan bahwa ada bug terbuka yang sangat mirip dengan yang Anda gambarkan. Intinya tampaknya ini adalah pilihan yang buruk tetapi tidak dapat diubah karena didokumentasikan di javadoc.


Untuk referensi, ini adalah kode removeAll(di Java 8 - belum memeriksa versi lain):

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;
}
assylias
sumber
15
Wow. Saya belajar sesuatu hari ini. Bagi saya, ini sepertinya pilihan penerapan yang buruk. Mereka tidak boleh melakukan itu jika koleksi lain bukan Kumpulan.
JB Nizet
2
@JBNizet Ya itu aneh - itu telah dibahas di sini dengan saran Anda - tidak yakin mengapa tidak berhasil ...
assylias
2
Terima kasih banyak @assylias ..Tapi benar-benar bertanya-tanya bagaimana Anda mengetahuinya .. :) Bagus sangat bagus .... Apakah Anda menghadapi masalah ini ???
8
@show_stopper Saya baru saja menjalankan profiler dan melihat ArrayList#containsitu pelakunya. Melihat kode AbstractSet#removeAllmemberikan sisa jawabannya.
assylias