假设我有一大堆相对较小的对象,我需要经常迭代。
我想通过提高缓存性能来优化迭代,所以我想在内存上连续分配对象 [而不是引用],这样我就可以减少缓存未命中数,整体表现可能会明显好转。
在C ++中,我可以只分配一个对象数组,它会按照我的意愿分配它们,但在java中 - 在分配一个数组时,我只分配引用,并且分配是在一个对象上完成的。时间。
我知道如果我“一次”分配对象[一个接一个],jvm 很可能将对象分配为尽可能连续,但它可能不是如果内存碎片化就足够了。
我的问题:
答案 0 :(得分:11)
新对象正在Eden空间中创建。伊甸园空间永远不会分散。 GC后它总是空的。
你遇到的问题是当执行GC时,对象可以随机排列在内存中,甚至可以按照相反的顺序排列。
解决方法是将字段存储为一系列数组。我将其称为基于列的表而不是基于行的表。
e.g。而不是写
class PointCount {
double x, y;
int count;
}
PointCount[] pc = new lots of small objects.
使用基于列的数据类型。
class PointCounts {
double[] xs, ys;
int[] counts;
}
或
class PointCounts {
TDoubleArrayList xs, ys;
TIntArrayList counts;
}
阵列本身最多可以在三个不同的位置,但数据总是连续的。如果您对字段子集执行操作,这甚至可以稍微提高效率。
public int totalCount() {
int sum = 0;
// counts are continuous without anything between the values.
for(int i: counts) sum += i;
return i;
}
我使用的解决方案是避免GC开销,因为大量数据是使用接口来访问直接或内存映射的ByteBuffer
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class MyCounters {
public static void main(String... args) {
Runtime rt = Runtime.getRuntime();
long used1 = rt.totalMemory() - rt.freeMemory();
long start = System.nanoTime();
int length = 100 * 1000 * 1000;
PointCount pc = new PointCountImpl(length);
for (int i = 0; i < length; i++) {
pc.index(i);
pc.setX(i);
pc.setY(-i);
pc.setCount(1);
}
for (int i = 0; i < length; i++) {
pc.index(i);
if (pc.getX() != i) throw new AssertionError();
if (pc.getY() != -i) throw new AssertionError();
if (pc.getCount() != 1) throw new AssertionError();
}
long time = System.nanoTime() - start;
long used2 = rt.totalMemory() - rt.freeMemory();
System.out.printf("Creating an array of %,d used %,d bytes of heap and tool %.1f seconds to set and get%n",
length, (used2 - used1), time / 1e9);
}
}
interface PointCount {
// set the index of the element referred to.
public void index(int index);
public double getX();
public void setX(double x);
public double getY();
public void setY(double y);
public int getCount();
public void setCount(int count);
public void incrementCount();
}
class PointCountImpl implements PointCount {
static final int X_OFFSET = 0;
static final int Y_OFFSET = X_OFFSET + 8;
static final int COUNT_OFFSET = Y_OFFSET + 8;
static final int LENGTH = COUNT_OFFSET + 4;
final ByteBuffer buffer;
int start = 0;
PointCountImpl(int count) {
this(ByteBuffer.allocateDirect(count * LENGTH).order(ByteOrder.nativeOrder()));
}
PointCountImpl(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void index(int index) {
start = index * LENGTH;
}
@Override
public double getX() {
return buffer.getDouble(start + X_OFFSET);
}
@Override
public void setX(double x) {
buffer.putDouble(start + X_OFFSET, x);
}
@Override
public double getY() {
return buffer.getDouble(start + Y_OFFSET);
}
@Override
public void setY(double y) {
buffer.putDouble(start + Y_OFFSET, y);
}
@Override
public int getCount() {
return buffer.getInt(start + COUNT_OFFSET);
}
@Override
public void setCount(int count) {
buffer.putInt(start + COUNT_OFFSET, count);
}
@Override
public void incrementCount() {
setCount(getCount() + 1);
}
}
使用-XX:-UseTLAB
选项运行(以获得准确的内存分配大小)打印
创建一个100,000,000的数组,使用了12,512个字节的堆,并花了1.8秒来设置和获取
作为它的off堆,它几乎没有GC影响。
答案 1 :(得分:0)
遗憾的是,无法确保在Java中创建/停留在相邻内存位置的对象。
但是,按顺序创建的对象很可能最终彼此相邻(当然这取决于实际的VM实现)。我很确定虚拟机的编写者都知道地方性非常受欢迎,并且不会随意散布对象。
垃圾收集器在某些时候可能会移动对象 - 如果你的对象是短暂的,那应该不是问题。对于长寿命对象,它取决于GC如何实现移动幸存者对象。再一次,我认为编写GC的人已经在这个问题上花了一些心思,并且会以一种不会使局部性变得不可避免的方式执行副本,这是合理的。
显然没有任何上述假设的保证,但由于我们无论如何都无法做任何事情,所以不要担心:)
你可以在java源代码级别做的唯一事情是有时避免对象的组合 - 而是你可以“内联”你通常放在复合对象中的状态:
class MyThing {
int myVar;
// ... more members
// composite object
Rectangle bounds;
}
代替:
class MyThing {
int myVar;
// ... more members
// "inlined" rectangle
int x, y, width, height;
}
当然,这会使代码的可读性降低,并且可能会复制很多代码。
通过访问模式对类成员进行排序似乎会产生轻微的影响(我注意到在重新排序某些声明后基准代码片段略有改动),但我从未打扰过验证如果它是真的。但如果虚拟机不对成员进行重新排序,那将是有意义的。
在同一主题上,(从性能视图)能够将现有基本数组重新解释为另一种类型(例如,将int []转换为float [])也是很好的。虽然你在这里,为什么不为工会成员呢?我确定。 但是我们必须放弃许多平台和架构独立性来换取这些可能性。
答案 2 :(得分:-3)
在Java中不起作用。迭代不是增加指针的问题。根据对象物理存储在堆上的位置没有性能影响。
如果您仍希望以C / C ++方式处理此问题,请将Java数组视为指向结构的指针数组。循环遍历数组时,分配实际结构的位置并不重要,而是循环遍历指针数组。
我会放弃这种推理。这不是Java的工作方式,也是次优化。