Phương thức HashSet <T> .removeAll chậm đáng kinh ngạc


92

Jon Skeet gần đây đã nêu ra một chủ đề lập trình thú vị trên blog của mình: "Có một lỗ hổng trong sự trừu tượng của tôi, Liza thân mến, Liza thân mến" (nhấn mạnh thêm):

Tôi có một bộ - một HashSet, trên thực tế. Tôi muốn xóa một số mục khỏi nó… và nhiều mục có thể không tồn tại. Trên thực tế, trong trường hợp thử nghiệm của chúng tôi, không có mục nào trong bộ sưu tập "loại bỏ" sẽ nằm trong bộ ban đầu. Điều này nghe có vẻ - và thực sự - cực kỳ dễ viết mã. Rốt cuộc, chúng tôi đã cóSet<T>.removeAll phải giúp chúng ta, phải không?

Chúng tôi chỉ định kích thước của tập hợp "nguồn" và kích thước của tập hợp "loại bỏ" trên dòng lệnh và xây dựng cả hai. Tập hợp nguồn chỉ chứa các số nguyên không âm; tập hợp loại bỏ chỉ chứa các số nguyên âm. Chúng tôi đo lường thời gian cần thiết để loại bỏ tất cả các yếu tố bằng cách sử dụng System.currentTimeMillis(), đây không phải là đồng hồ bấm giờ chính xác nhất thế giới nhưng là quá đủ trong trường hợp này, như bạn sẽ thấy. Đây là mã:

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

Hãy bắt đầu bằng cách tạo cho nó một công việc dễ dàng: một bộ nguồn gồm 100 mục và 100 mục để xóa:

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

Được rồi, vì vậy chúng tôi không mong đợi nó sẽ chậm… rõ ràng là chúng tôi có thể tăng cường mọi thứ lên một chút. Làm thế nào về nguồn một triệu mục và 300.000 mục cần loại bỏ?

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

Hừ! Điều đó vẫn có vẻ khá nhanh. Bây giờ tôi cảm thấy mình hơi tàn nhẫn, yêu cầu nó làm tất cả những điều đó. Hãy làm cho nó dễ dàng hơn một chút - 300.000 mục nguồn và 300.000 mục xóa:

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

Xin lỗi? Gần ba phút ? Rất tiếc! Chắc chắn sẽ dễ dàng hơn để xóa các mục khỏi bộ sưu tập nhỏ hơn bộ sưu tập mà chúng tôi quản lý trong 38 mili giây?

Ai đó có thể giải thích tại sao điều này đang xảy ra? Tại sao HashSet<T>.removeAllphương pháp này quá chậm?


2
Tôi đã kiểm tra mã của bạn và nó hoạt động nhanh. Đối với trường hợp của bạn, phải mất ~ 12ms để hoàn thành. Tôi cũng đã tăng cả hai giá trị đầu vào lên 10 và mất 36ms. Có thể PC của bạn thực hiện một số tác vụ CPU chuyên sâu trong khi bạn chạy các bài kiểm tra?
Slimu

4
Tôi đã thử nghiệm nó, và có kết quả tương tự như OP (tốt, tôi đã dừng nó trước khi kết thúc). Thực sự kỳ lạ. Windows, JDK 1.7.0_55
JB Nizet

2
Có một vé mở trên này: JDK-6982173
Haozhun

44
Như đã thảo luận trên Meta , câu hỏi này ban đầu được đạo văn từ blog của Jon Skeet (bây giờ được trích dẫn trực tiếp từ và liên kết đến trong câu hỏi, do chỉnh sửa của người kiểm duyệt). Các độc giả trong tương lai cần lưu ý rằng bài đăng trên blog mà nó bị ăn cắp trên thực tế giải thích nguyên nhân của hành vi, tương tự như câu trả lời được chấp nhận ở đây. Do đó, thay vì đọc câu trả lời ở đây, bạn có thể chỉ cần nhấp qua và đọc toàn bộ bài đăng trên blog .
Mark Amery

1
Lỗi sẽ được sửa trong Java 15: JDK-6394757
ZhekaKozlov

Câu trả lời:


138

Hành vi (phần nào) được ghi lại trong javadoc :

Việc triển khai này xác định cái nào là nhỏ hơn của tập hợp này và tập hợp được chỉ định, bằng cách gọi phương thức kích thước trên mỗi tập hợp. Nếu tập hợp này có ít phần tử hơn , thì việc triển khai lặp lại tập hợp này, lần lượt kiểm tra từng phần tử được trả về bởi trình lặp để xem liệu nó có được chứa trong tập hợp đã chỉ định hay không . Nếu nó được chứa như vậy, nó sẽ bị xóa khỏi tập hợp này bằng phương thức loại bỏ của trình lặp. Nếu tập hợp được chỉ định có ít phần tử hơn, thì việc triển khai lặp lại tập hợp được chỉ định, loại bỏ khỏi tập hợp này từng phần tử được trả về bởi trình vòng lặp, sử dụng phương pháp loại bỏ của tập hợp này.

Điều này có nghĩa là gì trong thực tế, khi bạn gọi source.removeAll(removals);:

  • nếu removalstập hợp có kích thước nhỏ hơn source, removephương thức của HashSetđược gọi là nhanh.

  • nếu removalstập hợp có kích thước bằng hoặc lớn hơn kích thước sourcethì removals.containsđược gọi, điều này là chậm đối với ArrayList.

Sửa nhanh:

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

Lưu ý rằng có một lỗi mở rất giống với những gì bạn mô tả. Điểm mấu chốt có vẻ là nó có lẽ là một lựa chọn tồi nhưng không thể thay đổi được vì nó được ghi lại trong javadoc.


Để tham khảo, đây là mã của removeAll(trong Java 8 - chưa kiểm tra các phiên bản khác):

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
Chà. Tôi đã học được vài điều hôm nay. Đây có vẻ là một lựa chọn triển khai tồi đối với tôi. Họ không nên làm điều đó nếu bộ sưu tập khác không phải là Bộ.
JB Nizet

2
@JBNizet Có đó là lạ - nó được thảo luận ở đây với đề nghị của bạn - không chắc chắn lý do tại sao nó không đi qua ...
assylias

2
Cảm ơn rất nhiều @assylias ..Nhưng thực sự tự hỏi làm thế nào bạn tìm ra nó .. :) Đẹp thực sự tốt đẹp .... Bạn đã đối mặt với vấn đề này ???

8
@show_stopper Tôi vừa chạy một hồ sơ và thấy đó ArrayList#containslà thủ phạm. Xem mã của AbstractSet#removeAllđã cho phần còn lại của câu trả lời.
assylias
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.