Java 哈希集代码的意外运行时间

Java 哈希集代码的意外运行时间,java,performance,for-loop,hashset,Java,Performance,For Loop,Hashset,所以最初,我有以下代码: 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); }

所以最初,我有以下代码:

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));
    }
}
import java.util.*;
公共类沙箱{
公共静态void main(字符串[]args){
HashSet HashSet=新的HashSet();
对于(int i=0;i<100_000;i++){
添加(i);
}
长启动=System.currentTimeMillis();
对于(int i=0;i<100_000;i++){
for(整数val:hashSet){
如果(val!=-1)中断;
}
hashSet.remove(i);
}
System.out.println(“时间:”+(System.currentTimeMillis()-start));
}
}
在我的计算机上运行嵌套for循环大约需要4秒,我不明白为什么需要这么长时间。外部循环运行100000次,内部for循环应该运行1次(因为hashSet的任何值都不会是-1),并且从hashSet中删除一个项是O(1),因此应该有大约200000个操作。如果一秒钟内通常有100000000个操作,为什么我的代码需要4s才能运行

另外,如果行
hashSet.remove(i)被注释掉,代码只需要16毫秒。

如果内部for循环被注释掉(但不是
hashSet.remove(i);
),代码只需要8毫秒。

您已经创建了
hashSet
的一个边际用例,其中算法降低为二次复杂度

以下是耗时很长的简化循环:

for (int i = 0; i < 100_000; i++) {
    hashSet.iterator().next();
    hashSet.remove(i);
}
突出显示的行是一个线性循环,用于搜索哈希表中的第一个非空bucket

由于
Integer
具有平凡的
hashCode
(即hashCode等于数字本身),结果表明,连续整数大多占据哈希表中的连续存储桶:数字0进入第一个存储桶,数字1进入第二个存储桶,等等

现在删除0到99999之间的连续数字。在最简单的情况下(当bucket包含一个键时),键的删除被实现为将bucket数组中的相应元素置零。请注意,移除后,工作台未压实或重新灰化

因此,从bucket数组开始移除的键越多,HashIterator查找第一个非空bucket所需的时间就越长

尝试从另一端移除键:

hashSet.remove(100_000 - i);

算法将大大加快

我确认你的发现。我可以推测原因,但希望聪明的人能给出一个有趣的解释。看起来val的
循环占用了很多时间。
删除
的速度仍然很快。在修改集合后设置新迭代器会产生某种开销…?@apangin在中很好地解释了
for val
循环速度慢的原因。但是,请注意,根本不需要循环。如果您想检查集合中是否有与-1不同的值,那么检查
hashSet.size()>1 | |将更加有效!hashSet.contains(-1)
。啊,我遇到了这个问题,但在最初几次运行后就放弃了它,并认为这可能是某种JIT优化,于是转向通过JITWatch进行分析。应该先运行异步探查器。该死很有趣。如果在循环中执行以下操作,它会通过减小内部映射的大小来加速循环:
If(i%800==0){hashSet=newhashset(hashSet);}
hashSet.remove(100_000 - i);