HashSet <T> .removeAllメソッドは驚くほど遅い


92

Jon Skeetは最近彼のブログで興味深いプログラミングトピックを取り上げました:「私の抽象化には、穴があります

私はセットを持っています– HashSet実際、。いくつかのアイテムを削除したいのですが、アイテムの多くが存在しない可能性があります。実際、私たちのテストケースでは、「removals」コレクションの項目はいずれも元のセットには含まれません。これ、実際にコーディングが非常に簡単に聞こえます。結局のところ、私たちは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

さて、それが遅くなるとは予想していませんでした…明らかに、少し物事を増やすことができます。100万個のアイテムと30万個のアイテムを削除するソースについてはどうですか?

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

うーん。それはまだかなり速いようです。今、私は少し残酷で、そのすべてを削除するように頼んでいると感じています。少し簡単にしましょう– 300,000のソースアイテムと300,000の削除:

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

すみません?約3 ?うわぁ!確かに、38ミリ秒で管理したものよりも小さなコレクションから項目を削除する方が簡単なはずです。

なぜこれが起こっているのか誰かが説明できますか?なぜHashSet<T>.removeAllメソッドはとても遅いのですか?


2
私はあなたのコードをテストしました、そしてそれは速く働きました。あなたの場合、完了するまでに約12msかかりました。また、両方の入力値を10増やしましたが、36ミリ秒かかりました。テストを実行している間、PCはいくつかの集中的なCPUタスクを実行しますか?
Slimu

4
私はそれをテストし、OPと同じ結果を得ました(まあ、私は終了前にそれを停止しました)。確かに奇妙です。Windows、JDK 1.7.0_55
JB

2
これにはオープンチケットがあります:JDK-6982173
Haozhun

44
メタに議論し、この質問はもともと(今から直接引用し、問題ににリンクされ、司会の編集による)ジョンスキートのブログから盗用されました。今後の読者は、盗用されたブログ投稿が、ここで受け入れられた回答と同様に、実際に動作の原因を説明していることに注意する必要があります。そのため、ここで回答を読む代わりに、単にクリックしてブログ記事全体を読むこともできます。
Mark Amery、

1
このバグはJava 15で修正されます:JDK-6394757
ZhekaKozlov

回答:


138

動作はjavadocに(ある程度)記載されています。

この実装は、それぞれのsizeメソッドを呼び出すことにより、このセットと指定されたコレクションのどちらが小さいかを判断します。このセットの要素が少ない場合、実装はこのセットを反復処理し、イテレータによって返された各要素を順番にチェックして、指定されたコレクションに含まれているかどうかを確認します。含まれている場合は、イテレータのremoveメソッドを使用してこのセットから削除されます。指定されたコレクションに含まれる要素の数が少ない場合、実装は指定されたコレクションを反復処理し、このセットのremoveメソッドを使用して、反復子によって返された各要素をこのセットから削除します。

これが実際に何を意味するか、あなたが呼び出すときsource.removeAll(removals);

  • 場合はremovals、コレクションは、より小さなサイズのものであるsourceremove方法HashSet高速で呼ばれています、。

  • 場合removalsコレクションがより等しいか大きいサイズであるsource場合、removals.containsArrayListのために遅いである、と呼ばれています。

クイックフィックス:

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ニゼット2015

2
@JBNizetはい、それは奇妙です-それはあなたの提案でここで議論されました -それがうまくいかなかった理由がわからない...
assylias

2
本当にありがとう@assylias ..しかし、どうやってそれを理解したのだろうと本当に思った.. :)ニース本当にいい....この問題に直面したか???

8
@show_stopperプロファイラーを実行したところ、それArrayList#containsが原因であることがわかりました。のコードを見て、AbstractSet#removeAll残りの答えを与えました。
アッシリア2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.