我有一个Iterable< T>的实现. (四叉树结构的一种变体),我计划在大型数据集的性能至关重要的环境中使用,所以我一直在进行一些测试,有几百万个随机条目,重复运行它们.我对以下代码段感到奇怪:
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
Iterator iter = it.iterator();
while (iter.hasNext()) {
iter.next();
}
}
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start));
我总是有4000到5000毫秒的时间.但是,当我将while循环更改为:
A a = null;
while (iter.hasNext()) {
a = iter.next();
}
时间跳跃 – 不仅仅是轻微的,而是一直到15到16秒,完全一致.现在这已经不依赖于next()的实现了,但经过进一步调查,我发现它甚至发生在一个简单的ArrayList中,所以我将发布可编译的代码:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test {
static class A {}
public static void main(String[] args) {
List list = new ArrayList<>();
// Add a lot of entries
for (int i = 0; i < 10000000; i++) {
list.add(new A());
}
// Test it
A a = null;
Iterator iter = null;
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
iter = list.iterator();
while (iter.hasNext()) {
iter.next();
// Or:
// a = iter.next();
}
}
long end = System.currentTimeMillis();
System.out.println("Total time: " + (end - start));
}
}
结果:更令人难以置信的30倍差异.并且每次都确定地发生.
可能的原因是什么?我没有看到对已经分配的变量的单个赋值如何可以是除了可忽略的以外的任何值,特别是考虑到iter.next()中发生了太多其他事情.我唯一的猜测是System.currentTimeMillis()调用在某种程度上没有在适当的时候执行,但至于如何受到更改的影响,我不知道.
但即使这样也不太合适,因为它花费的时间明显多得多,特别是如果我进一步增加for循环运行的次数.据我所知,垃圾收集器也不应该做任何事情,因为不应该发生浪费的临时分配.它显然也是返回值的分配,因为除了iter.next()之外只做其他事情,比如每次增加一个int变量,对执行时间没有相同的负面影响.
编辑:多个人已经注意到我的帖子中的特定基准遭受了许多可能影响其结果可信度的问题.我会把它留在这里作为后代,或者可能稍后更新以使其更好.有人说过,这个现象最可能的原因已在接受的答案中得到确认,我确认消除类型转换解决了这个问题,所以尽管基准测试的缺点,上面的观察结果似乎并不仅仅是那些副作用.
解决方法:
我认为您看到的许多差异将取决于您的基准测试方式.我没有看到您尝试处理JVM预热效果或隔离GC和内存分配效果的迹象.甚至是内存缓存大小的影响.
但我想我知道无论如何会发生什么.
和…之间的不同
while (iter.hasNext()) {
iter.next();
}
和
A a = null;
while (iter.hasNext()) {
a = iter.next();
}
是(显然!)任务.但是赋值也有一个隐藏类型转换来检查next()返回的值是否真的是A.(提示:泛型类型擦除…)
但这种类型演员怎么会花那么多时间呢?
好吧,我的理论是,这是类型转换本身的成本和内存缓存/位置效应的组合.
在第一个示例中,迭代是从大型数组中顺序读取引用.这是一个相对缓存友好的事情…因为数组将是内存中的单个连续块,并且硬件易于在单个操作中将多个字提取到缓存中. (实际上,JIT甚至可以发出缓存预取指令……以避免流水线停滞.(这是猜测…))
在第二个例子中,在读取每个引用之间,CPU也将进行类型转换.类型转换涉及从每个A实例的头部检索类标识符,然后测试它是否是正确的.
>从对象标头中检索标识符是每次从内存的不同部分获取内存.对象可能在内存中开始连续,但即使如此,间距也可能是多个单词.缓存效果会差得多.甚至数组和对象都通过相同的缓存这一事实也很重要.
>测试类标识符可能非常重要.如果A是类而不是接口而且它没有子类,那么运行时应该能够执行等效的== test.否则,测试将更复杂,更昂贵.
第二种可能的解释与代码内联有关.如果Iterator :: next()调用足够小以便内联,那么JIT编译器的窥视孔优化器可能能够推断出在无代码版本的代码中,部分或全部下一个代码是冗余的.但是,我怀疑它可以推断next()完全是多余的,因为并发修改检查.消除这些检查会改变边缘情况下的代码行为,并且将是无效的优化.
简而言之,不难看出赋值和相关的隐藏类型组的添加如何对性能产生重大影响,特别是对于大型数据结构.
标签:java,variable-assignment,performance
来源: https://codeday.me/bug/20190527/1162049.html