【问题标题】:Unexpected running times for HashSet codeHashSet 代码的意外运行时间
【发布时间】:2020-04-18 16:28:21
【问题描述】:

原来,我有这个代码:

import java.util.*;

public class sandbox {
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 100_000; i++) {
            hashSet.add(i);
        }

        long start = System.currentTimeMillis();

        for (int i = 0; i < 100_000; i++) {
            for (Integer val : hashSet) {
                if (val != -1) break;
            }

            hashSet.remove(i);
        }

        System.out.println("time: " + (System.currentTimeMillis() - start));
    }
}

在我的计算机上运行嵌套的 for 循环大约需要 4 秒,我不明白为什么要花这么长时间。外部循环运行 100,000 次,内部 for 循环应该运行 1 次(因为 hashSet 的任何值都不会是 -1)并且从 HashSet 中删除一个项目是 O(1),所以应该有大约 200,000 次操作。如果一秒钟内通常有 100,000,000 次操作,那我的代码为什么需要 4s 才能运行?

另外,如果hashSet.remove(i);这一行被注释掉,代码只需要16ms。 如果内部的for循环被注释掉了(而不是hashSet.remove(i);),代码只需要8ms。

【问题讨论】:

  • 我确认您的发现。我可以推测原因,但希望聪明的人会发表一个有趣的解释。
  • 看起来for val 循环是占用时间的事情。 remove 仍然非常快。修改集合后设置新迭代器的某种开销...?
  • @apangin 在stackoverflow.com/a/59522575/108326 中很好地解释了为什么for val 循环很慢。但是,请注意,根本不需要循环。如果要检查集合中是否有任何不同于-1的值,检查hashSet.size() &gt; 1 || !hashSet.contains(-1)会效率更高。

标签: java performance for-loop hashset


【解决方案1】:

您创建了HashSet 的边缘用例,其中算法降级为二次复杂度。

这是需要很长时间的简化循环:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}

async-profiler 表明几乎所有时间都花在 java.util.HashMap$HashIterator() 构造函数中:

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
--->        do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

突出显示的行是一个线性循环,用于搜索哈希表中的第一个非空桶。

由于Integer有琐碎的hashCode(即hashCode等于数字本身),原来连续整数大多占据哈希表中连续的桶:数字0去第一个桶,数字1去第二个桶,等等。

现在您删除从 0 到 99999 的连续数字。在最简单的情况下(当存储桶包含单个键时),删除键是通过将存储桶数组中的相应元素清空来实现的。请注意,表格在删除后不会被压缩或重新散列。

因此,您从存储桶数组的开头删除的键越多,HashIterator 需要找到第一个非空存储桶的时间就越长。

尝试从另一端移除钥匙:

hashSet.remove(100_000 - i);

算法将变得更快!

【讨论】:

  • 啊,我遇到了这个问题,但在前几次运行后将其驳回,并认为这可能是一些 JIT 优化并转向通过 JITWatch 进行分析。应该先运行 async-profiler。该死!
  • 很有趣。如果您在循环中执行以下操作,它会通过减小内部映射的大小来加快速度:if (i % 800 == 0) { hashSet = new HashSet&lt;&gt;(hashSet); }.
猜你喜欢
  • 1970-01-01
  • 2020-03-09
  • 2020-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-12
  • 1970-01-01
相关资源
最近更新 更多