HashSet代码的意外运行时间


28

因此,最初,我有以下代码:

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次操作,我的代码为什么要花4秒钟才能运行?

此外,如果hashSet.remove(i);注释掉该行,则代码仅需16ms。如果内部的for循环被注释掉了(但未注释掉hashSet.remove(i);),则代码仅需8ms。


4
我确认你的发现。我可以推测其原因,但希望有人会发表有趣的解释。
khelwood

1
看起来for val循环是占用时间的事情。该remove还是非常快的。在修改集合后需要某种开销来设置新的迭代器...?
khelwood

@apangin在stackoverflow.com/a/59522575/108326中提供了一个很好的解释,说明了for val循环缓慢的原因。但是,请注意,根本不需要循环。如果您要检查集合中是否有不同于-1的值,则检查效率会更高hashSet.size() > 1 || !hashSet.contains(-1)
markusk

Answers:


32

您已经创建了一个边际用例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);

该算法将变得更快!


1
啊,我遇到了这个问题,但是在前几次运行后就忽略了它,并认为这可能是一些JIT优化,并通过JITWatch进行了分析。应该先运行async-profiler。该死的!
等候Kumar

1
非常有趣。如果您在循环中执行以下操作,则会通过减小内部地图的大小来加快速度if (i % 800 == 0) { hashSet = new HashSet<>(hashSet); }
灰色–所以别再邪恶
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.