假设您正在对一大组大型浮点矢量进行一些计算,例如:计算每个的平均值:
public static float avg(float[] data, int offset, int length) {
float sum = 0;
for (int i = offset; i < offset + length; i++) {
sum += data[i];
}
return sum / length;
}
如果您将所有向量存储在内存中的float []中,则可以按如下方式实现循环:
float[] data; // <-- vectors here
float sum = 0;
for (int i = 0; i < nVectors; i++) {
sum += avg(data, i * vectorSize, vectorSize);
}
如果您的向量存储在一个文件中,那么内存映射它应该与第一个解决方案一样快,理论上,一旦操作系统缓存了整个事情:
RandomAccessFile file; // <-- vectors here
MappedByteBuffer buffer = file.getChannel().map(READ_WRITE, 0, 4*data.length);
FloatBuffer floatBuffer = buffer.asFloatBuffer();
buffer.load(); // <-- this forces the OS to cache the file
float[] vector = new float[vectorSize];
float sum = 0;
for (int i = 0; i < nVectors; i++) {
floatBuffer.get(vector);
sum += avg(vector, 0, vector.length);
}
但是,我的测试显示内存映射版本比内存映射版本慢约5倍.我知道FloatBuffer.get(float [])正在复制内存,我想这就是放慢速度的原因.可以更快吗?有没有办法避免任何内存复制,只是从操作系统缓冲区获取数据?
我已将我的完整基准上传到this gist,以防您想尝试运行:
$java -Xmx1024m ArrayVsMMap 100 100000 100
编辑:
最后,在这种情况下我能够摆脱MappedByteBuffer的最好速度仍然比使用常规float []慢~35%.到目前为止的技巧是:
>使用本机字节顺序来避免转换:buffer.order(ByteOrder.nativeOrder())
>使用buffer.asFloatBuffer()使用FloatBuffer包装MappedByteBuffer
>使用简单的floatBuffer.get(int index)而不是批量版本,这可以避免内存复制.
您可以在this gist看到新的基准和结果.
1.35的减速比5中的任何一个要好得多,但它仍然远离1.我可能仍然缺少一些东西,否则它应该是JVM中应该改进的东西.
解决方法:
基于阵列的时间非常快!每个浮点数我得到0.0002纳秒. JVM可能正在优化循环.
这就是问题:
void iterate() {
for (int i = 0; i < nVectors; i++) {
calc(data, i * vectorSize, vectorSize);
}
}
JVM意识到calc没有副作用,因此iterate也没有,所以它可以用NOP替换.一个简单的解决方法是累积calc的结果并返回它.您还需要对定时循环中的迭代结果执行相同的操作,并打印结果.这可以防止优化器删除所有代码.
编辑:
这看起来可能只是Java方面的开销,与内存映射本身无关,只与它的接口有关.尝试以下测试,它只围绕字节[]围绕ByteBuffer包装一个FloatBuffer:
private static final class ArrayByteBufferTest extends IterationTest {
private final FloatBuffer floatBuffer;
private final int vectorSize;
private final int nVectors;
ArrayByteBufferTest(float[] data, int vectorSize, int nVectors) {
ByteBuffer bb = ByteBuffer.wrap(new byte[data.length * 4]);
for (int i = 0; i < data.length; i++) {
bb.putFloat(data[i]);
}
bb.rewind();
this.floatBuffer = bb.asFloatBuffer();
this.vectorSize = vectorSize;
this.nVectors = nVectors;
}
float iterate() {
float sum = 0;
floatBuffer.rewind();
float[] vector = new float[vectorSize];
for (int i = 0; i < nVectors; i++) {
floatBuffer.get(vector);
sum += calc(vector, 0, vector.length);
}
return sum;
}
}
由于你在浮动本身上做了很少的工作(只需添加它,可能是1个周期),读取4个字节,构建浮点数并将其复制到数组的成本都会增加.我注意到它有助于开销有一些更少,更大的向量,至少直到向量大于(L1?)高速缓存.