HashSet<T>.removeAll 方法非常慢
Posted
技术标签:
【中文标题】HashSet<T>.removeAll 方法非常慢【英文标题】:The HashSet<T>.removeAll method is surprisingly slow 【发布时间】:2015-04-24 15:58:33 【问题描述】:Jon Skeet 最近在他的博客上提出了一个有趣的编程话题:"There's a hole in my abstraction, dear Liza, dear Liza"(添加了重点):
我有一套——事实上是
HashSet
。我想从中删除一些项目……而且许多项目可能不存在。事实上,在我们的测试用例中,“removals”集合中的 none 项将在原始集合中。这听起来 - 确实是 - 非常容易编码。毕竟,我们有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
对不起?将近三 分钟?哎呀!与我们在 38 毫秒内管理的集合相比,从 更小的 集合中删除项目确实应该更容易?
有人可以解释为什么会这样吗?为什么HashSet<T>.removeAll
方法这么慢?
【问题讨论】:
我测试了你的代码,它运行得很快。对于您的情况,完成大约需要 12 毫秒。我还将两个输入值都增加了 10,它花了 36 毫秒。也许您的 PC 在您运行测试时会执行一些密集的 CPU 任务? 我测试了它,结果与 OP 相同(好吧,我在结束前停止了它)。确实很奇怪。视窗,JDK 1.7.0_55 有一张公开的票:JDK-6982173 作为discussed on Meta,这个问题最初是从 Jon Skeet 的博客中抄袭的(由于版主的编辑,现在直接引用并链接到问题中)。未来的读者应该注意,它被剽窃的博客文章实际上解释了行为的原因,类似于这里接受的答案。因此,您可能不想在这里阅读答案,而是希望简单地点击并阅读the full blog post。 该错误将在 Java 15 中修复:JDK-6394757 【参考方案1】:该行为(某种程度上)记录在javadoc:
此实现通过在每个集合上调用 size 方法来确定该集合和指定集合中的较小者。 如果这个集合有更少的元素,那么实现会迭代这个集合,依次检查迭代器返回的每个元素以查看是否包含在指定的集合中。如果它如此包含,则使用迭代器的 remove 方法将其从该集合中删除。如果指定集合的元素较少,则实现对指定集合进行迭代,使用该集合的 remove 方法从该集合中移除迭代器返回的每个元素。
这在实践中意味着什么,当你打电话给source.removeAll(removals);
时:
如果removals
集合的大小小于source
,则调用HashSet
的remove
方法,速度很快。
如果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;
【讨论】:
哇。我今天学到了一些东西。对我来说,这看起来是一个糟糕的实现选择。如果其他集合不是 Set,他们不应该这样做。 @JBNizet 是的,这很奇怪 - 您的建议已经在 here 进行了讨论 - 不知道为什么它没有通过... 非常感谢@assylias ..但真的很想知道你是怎么想出来的..:) 很好很好......你遇到过这个问题吗??? @show_stopper 我刚刚运行了一个分析器,发现ArrayList#contains
是罪魁祸首。看一下AbstractSet#removeAll
的代码就给出了剩下的答案。以上是关于HashSet<T>.removeAll 方法非常慢的主要内容,如果未能解决你的问题,请参考以下文章