HashSet <t> .removeAll方法非常慢</t>

时间:2015-02-23 10:45:20

标签: java performance collections hashset

Jon Skeet最近在他的博客上提出了一个有趣的编程主题:"There's a hole in my abstraction, dear Liza, dear Liza"(重点补充):

  

事实上我有一套 - HashSet。我想从中删除一些项目...许多项目可能根本不存在。事实上,在我们的测试用例中,“removals”集合中的项目的 none 将在原始集合中。这听起来 - 实际上 - 非常容易编码。毕竟,我们有Set<T>.removeAll来帮助我们,对吧?

     

我们在命令行中指定“source”集的大小和“removals”集合的大小,并构建它们。源集仅包含非负整数;删除集仅包含负整数。我们使用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方法这么慢?

1 个答案:

答案 0 :(得分:95)

行为(有些)记录在javadoc

  

此实现通过在每个集合上调用size方法来确定该集合和指定集合中较小的集合。 如果此集合具有较少的元素 ,则实现将迭代此集合,依次检查迭代器返回的每个元素,以查看 是否包含在指定的集合中 。如果包含它,则使用迭代器的remove方法从该集合中删除它。如果指定的集合具有较少的元素,则实现将迭代指定的集合,使用此set的remove方法从迭代器返回的每个元素中删除此集合。

这在实践中意味着,当您致电source.removeAll(removals);时:

  • 如果removals集合的大小小于source,则会调用remove HashSet方法,这很快。

  • 如果removals集合的大小等于或大于source,则调用removals.contains,这对于ArrayList来说很慢。

快速修复:

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

请注意,an open bug与您描述的内容非常相似。底线似乎是它可能是一个糟糕的选择但不能改变,因为它在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;
}