加快Java的文件序列化速度

tsl0922 发布于 2012/10/06 12:00
阅读 10K+
收藏 143

自从第一个Java版本开始,很多开发人员一直都在尝试让Java获得最少和C/C++一样的表现。JVM提供商尽他们最大的努力去实现一些新的JIT算法,但是还是有很多需要做的,特别是在我们使用Java的方法上。

例如,在对象<->文件序列化上就差距很大--尤其在读写内存对象上。我将就这个主题做一些解释和分享。

所有的测试都是在下面这个对象上执行的:

public class TestObject implements Serializable {

  private long longVariable;
  private long[] longArray;
  private String stringObject;
  private String secondStringObject; //just for testing nulls

  /* getters and setters */
}

为了简单起见,我将只贴出写入方法(尽管读取类似),完整的源码在我的GitHub上可以找到(http://github.com/jkubrynski/serialization-tests

最标准的java序列化(我们都是从这里学起的)是这样的:

public void testWriteBuffered(TestObject test, String fileName) throws IOException {
  ObjectOutputStream objectOutputStream = null;
  try {
    FileOutputStream fos = new FileOutputStream(fileName);
    BufferedOutputStream bos = new BufferedOutputStream(fos);
    objectOutputStream = new ObjectOutputStream(bos);
    objectOutputStream.writeObject(test);
  } finally {
    if (objectOutputStream != null) {
      objectOutputStream.close();
    }
  }
}

提升标准序列化速度的最简单方法时使用RandomAccessFile对象:

public void testWriteBuffered(TestObject test, String fileName) throws IOException {
  ObjectOutputStream objectOutputStream = null;
  try {
    RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
    FileOutputStream fos = new FileOutputStream(raf.getFD());
    objectOutputStream = new ObjectOutputStream(fos);
    objectOutputStream.writeObject(test);
  } finally {
    if (objectOutputStream != null) {
      objectOutputStream.close();
    }      
}

更高深点的技术是使用Kryo框架,新旧版本的差距是很大的,我做过测试。因为性能比较上并没有体现出特别引人注意的差异,所以我将使用2.x版本,因为它对用户更友好而且更快些。

private static Kryo kryo = new Kryo(); // version 2.x

public void testWriteBuffered(TestObject test, String fileName) throws IOException {
  Output output = null;
  try {
    RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
    output = new Output(new FileOutputStream(raf.getFD()), MAX_BUFFER_SIZE);
    kryo.writeObject(output, test);
  } finally {
    if (output != null) {
      output.close();
    }
  }
}

最后一个方案是在Martin Thompson的文章中提到的(Native C/C++ Like Performance For Java Object Serialisation),介绍了怎样在Java中像C++那样和内存打交道。

public void testWriteBuffered(TestObject test, String fileName) throws IOException {
  RandomAccessFile raf = null;
  try {
    MemoryBuffer memoryBuffer = new MemoryBuffer(MAX_BUFFER_SIZE);
    raf = new RandomAccessFile(fileName, "rw");
    test.write(memoryBuffer);
    raf.write(memoryBuffer.getBuffer());
  } catch (IOException e) {
    if (raf != null) {
      raf.close();
    }
  }
}

TestObject写入方法如下:

public void write(MemoryBuffer unsafeBuffer) {
  unsafeBuffer.putLong(longVariable);
  unsafeBuffer.putLongArray(longArray);
  // we support nulls
  boolean objectExists = stringObject != null;
  unsafeBuffer.putBoolean(objectExists);
  if (objectExists) {
    unsafeBuffer.putCharArray(stringObject.toCharArray());
  }
  objectExists = secondStringObject != null;
  unsafeBuffer.putBoolean(objectExists);
  if (objectExists) {
    unsafeBuffer.putCharArray(secondStringObject.toCharArray());
  }
} 

直接内存缓冲区类(已简化了的,仅仅为了展示这个思想)

public class MemoryBuffer {
  // getting Unsafe by reflection
  public static final Unsafe unsafe = UnsafeUtil.getUnsafe();

  private final byte[] buffer;

  private static final long byteArrayOffset = unsafe.arrayBaseOffset(byte[].class);
  private static final long longArrayOffset = unsafe.arrayBaseOffset(long[].class);
  /* other offsets */

  private static final int SIZE_OF_LONG = 8;
  /* other sizes */

  private long pos = 0;

  public MemoryBuffer(int bufferSize) {
    this.buffer = new byte[bufferSize];
  }

  public final byte[] getBuffer() {
    return buffer;
  }

  public final void putLong(long value) {
    unsafe.putLong(buffer, byteArrayOffset + pos, value);
    pos += SIZE_OF_LONG;
  }

  public final long getLong() {
    long result = unsafe.getLong(buffer, byteArrayOffset + pos);
    pos += SIZE_OF_LONG;
    return result;
  }

  public final void putLongArray(final long[] values) {
    putInt(values.length);
    long bytesToCopy = values.length << 3;
    unsafe.copyMemory(values, longArrayOffset, buffer, byteArrayOffset + pos, bytesToCopy);
    pos += bytesToCopy;
  }


  public final long[] getLongArray() {
    int arraySize = getInt();
    long[] values = new long[arraySize];
    long bytesToCopy = values.length << 3;
    unsafe.copyMemory(buffer, byteArrayOffset + pos, values, longArrayOffset, bytesToCopy);
    pos += bytesToCopy;
    return values;
  }

  /* other methods */
}

几个小时的Caliper测试结果如下:

  Full trip [ns]  Standard deviation [ns] 
Standard   207307  2362
Standard on RAF  42661  733
KRYO 1.x   12027  112
KRYO 2.x  11479  259
Unsafe  8554  91

在最后我们可以得出一些结论:

  • Unsafe序列化比标准的java.io.Serizlizable快了23倍
  • 使用RandomAccessFile可以使标准的有缓冲序列化加速将近4倍
  • Kryo-dynamic序列化大约比手写实现的直接缓冲满了35%

最后,就像我们看到的那样,还是没有绝对的答案。对于我们中的大多数人来说,获得3000ns(0.003ms)的速度提升是不值得为每个需要序列化的对象来写单独实现的。在标准的方案中,我们大多数选择Kryo 。然而,在惜时如金的低延时系统中,这个选择将会是完全不同的。

OSCHINA编译,原文链接

加载中
0
frankiegao123
frankiegao123
既然叫 Unsafe 那也就是说 Sun 的 JRE 实现,不希望应用开发人员去使用它的,呵呵
0
mj4738
mj4738
即使是(准)实时系统,也可以用其他方式代替对象序列化吧。不过还是mark一下
0
0xAB
0xAB
google go也有个unsafe库,专门用于和c/c++接口程序打交道。照样用
0
唯一
唯一
Kryo-dynamic序列化大约比手写实现的直接缓冲满了35%   是慢吗?
返回顶部
顶部