MappedByteBuffer.asFloatBuffer()与内存中的float []性能

时间:2012-08-26 18:06:42

标签: java performance nio mmap memory-mapped-files

假设您正在对一大组大型float向量进行一些计算,例如:计算每个的平均值:

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())
  • 使用MappedByteBuffer
  • FloatBufferbuffer.asFloatBuffer()打包在一起
  • 使用简单的floatBuffer.get(int index)而不是批量版本,这可以避免内存复制。

您可以在this gist看到新的基准和结果。

1.35的减速比5中的任何一个要好得多,但它仍然远离1.我可能仍然遗漏了一些东西,或者它是JVM中应该改进的东西。

2 个答案:

答案 0 :(得分:3)

基于数组的时间非常快!每个浮点数我得到0.0002纳秒。 JVM可能正在优化循环。

这是问题所在:

    void iterate() {
        for (int i = 0; i < nVectors; i++) {
            calc(data, i * vectorSize, vectorSize);
        }
    }

JVM意识到calc没有副作用,因此iterate也没有副作用,因此可以用NOP替换它。一个简单的解决方法是累积calc的结果并将其返回。您还需要在计时循环中对iterate的结果执行相同操作,然后打印结果。这会阻止优化器删除所有代码。

修改

这看起来可能只是Java方面的开销,与内存映射本身无关,只与它的接口有关。尝试以下测试,该FloatBuffer围绕ByteBuffer byte[]周围的 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}}

由于你在浮动本身上做了很少的工作(只需添加它,可能是1个周期),读取4个字节,构建浮点数并将其复制到数组的成本都会增加。我注意到它有助于增加更少,更大的向量的开销,至少在向量大于(L1?)高速缓存之前。

答案 1 :(得分:0)

理论上没有理由他们应该这样做。映射的解决方案意味着页面错误和磁盘I / O到完全不可预测的程度。 float []数组没有。您应该期望后者更快,除非在整个文件映射到内存的特殊情况下,您永远不会更改它它保持映射并且永远不会被分页。大多数这些因素是你无法控制或预测的。