我很想知道heap write traffic的含义以及为什么在ArrayList实现中需要它?
ArrayList
实施的摘录,请参阅注释
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
答案 0 :(得分:2)
可能是作者希望使用局部变量i
,因为在escape analysis and stack allocation启动时可能会分配堆栈。与cursor
不同,i
变量已更改多次由于i++
循环内的while
语句。在堆栈上增加它并且跳过所有Java内存模型含义应该更便宜。 Iterator.cursor
是一个成员字段,它可能总是在堆上,尤其是Iterator
个对象在用户代码中传递。
答案 1 :(得分:2)
通常cursor
变量指向Iterator
要返回的下一个元素。因此,在迭代时,您需要每次都更新cursor
变量,以便它始终指向正确的元素。
但是,forEachRemaining
方法自行完成迭代。这并不意味着暂停。因此,您可以忽略更新游标变量,直到方法完成。当方法迭代时,cursor
将指向错误的元素。但是因为你不能暂停这个方法,所以没有任何区别。
通过这种方式,您可以将分配量减少到cursor
,并减少堆流量。所以他们引用更正确的实现,如
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
// Update cursor while iterating
cursor = i;
}
或直接使用光标而不是额外的i
while (cursor != size && modCount == expectedModCount) {
consumer.accept((E) elementData[cursor++]);
}
但是你要处理成员变量而不是局部变量i
。与i
合作更便宜,有关详细信息,请参阅@kdowbecki的答案。
答案 2 :(得分:1)
如果您忽略所有保护条件,next()
会执行以下操作:
public E next() {
Object[] elementData = ArrayList.this.elementData;
int i = cursor;
cursor = i + 1;
lastRet = i;
return (E) elementData[i];
}
forEachRemaining()
基本上会继续调用next()
并在每个元素上调用使用者,所以如果我们这样做,则内联next()
逻辑,我们得到:
public void forEachRemaining(Consumer<? super E> consumer) {
final int size = ArrayList.this.size;
final Object[] elementData = ArrayList.this.elementData;
int i = cursor;
while (i != size) { // same as hasNext()
// begin: consumer.accept(next())
cursor = i + 1;
lastRet = i;
consumer.accept((E) elementData[i]);
// end: consumer.accept(next())
i++;
}
}
由于cursor
和lastRet
都是字段,因此它们存在于堆上,而堆栈中存在i
。
为减少内存写入次数,cursor
和lastRet
的更新可以移出循环,因为它们实际上并未在循环中使用。
当然,您现在在一次额外i++
之后再这样做了,因此您需要从1
中减去i
。
public void forEachRemaining(Consumer<? super E> consumer) {
final int size = ArrayList.this.size;
final Object[] elementData = ArrayList.this.elementData;
int i = cursor;
while (i != size) {
consumer.accept((E) elementData[i++]);
}
cursor = i;
lastRet = i - 1;
}
效果是在迭代期间只更新堆栈变量i
,并且两个堆值保持不变。
一旦JIT启动,如果内联accept()
调用,堆栈变量i
甚至可能被删除并变成一个寄存器值,大大减少了更新的次数。 #34;存储器中。
答案 3 :(得分:1)
感谢您提供答案。为了更好地解释,我将添加内部Java内存模型的外观,如下图所示。正如@kdowbecki,@ Zabuza和@Andreas指出的那样,使用Thread Stack
内存进行本地执行然后在每次迭代中使用Heap
内存是有效的。它可能属于amortized analysis类别。
在操作系统中检查memory model of a process(JVM是一个进程)也很有趣。