web-dev-qa-db-ja.com

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

Jon Skeetは最近、彼のブログで興味深いプログラミングトピックを取り上げました: 「私の抽象化には穴があります、親愛なるライザ、親愛なるライザ」 (強調追加):

実際、HashSetというセットがあります。そこからいくつかのアイテムを削除したい…そして、多くのアイテムは存在しないかもしれない。実際、テストケースでは、「removals」コレクションのアイテムのnoneは元のセットに含まれます。これは、実際にisで、非常に簡単にコーディングできます。結局、私たちは _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万アイテムと300,000アイテムのソースはどうですか?

_c:UsersJonTest>Java Test 1000000 300000
Time taken: 38ms
_

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

_c:UsersJonTest>Java Test 300000 300000
Time taken: 178131ms
_

すみません?ほぼ3分ですか?うわぁ!確かに、38msで管理したものよりも小さいコレクションからアイテムを削除する方が簡単でしょうか?

誰かがこれが起こっている理由を説明できますか? _HashSet<T>.removeAll_メソッドが非常に遅いのはなぜですか?

63
anon

動作は javadoc に(ある程度)文書化されています:

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

source.removeAll(removals);を呼び出すときの実際の意味:

  • removalsコレクションのサイズがsourceよりも小さい場合、removeHashSetメソッドが呼び出されますが、これは高速です。

  • 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;
}
97
assylias