4

假设您正在对一大组大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[])是在复制内存,我想这就是速度变慢的原因。能不能快点?有没有办法完全避免任何内存复制,只从操作系统的缓冲区中获取我的数据?

我已经将我的完整基准上传到这个 gist,以防你想尝试它只是运行:

$ java -Xmx1024m ArrayVsMMap 100 100000 100

编辑:

最后,在这种情况下,我能够摆脱的最好结果MappedByteBuffer仍然比使用常规慢float[]约 35%。到目前为止的技巧是:

  • 使用本机字节顺序来避免转换:buffer.order(ByteOrder.nativeOrder())
  • MappedByteBufferFloatBufferusing包裹buffer.asFloatBuffer()
  • 使用简单floatBuffer.get(int index)版本而不是批量版本,这样可以避免内存复制。

您可以在这个要点上看到新的基准和结果。

1.35 的减速比 5 中的一个要好得多,但距离 1 还很远。我可能仍然缺少一些东西,或者它是 JVM 中应该改进的东西。

4

2 回答 2

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 端的开销,与内存映射本身无关,只是它的接口。尝试以下测试,它只是将 a 包裹FloatBuffer在 aByteBuffer周围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 个周期),因此读取 4 个字节、构建浮点数并将其复制到数组的成本加起来。我注意到它有助于开销更少,更大的向量,至少在向量大于(L1?)缓存之前。

于 2012-08-26T22:58:54.947 回答
0

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

于 2012-08-27T00:35:06.563 回答