加快Java的文件序列化速度

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

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

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

1 public class TestObject implements Serializable {
2  
3   private long longVariable;
4   private long[] longArray;
5   private String stringObject;
6   private String secondStringObject; //just for testing nulls
7  
8   /* getters and setters */
9 }

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

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

01 public void testWriteBuffered(TestObject test, String fileName) throwsIOException {
02   ObjectOutputStream objectOutputStream = null;
03   try {
04     FileOutputStream fos = new FileOutputStream(fileName);
05     BufferedOutputStream bos = new BufferedOutputStream(fos);
06     objectOutputStream = new ObjectOutputStream(bos);
07     objectOutputStream.writeObject(test);
08   finally {
09     if (objectOutputStream != null) {
10       objectOutputStream.close();
11     }
12   }
13 }

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

01 public void testWriteBuffered(TestObject test, String fileName) throwsIOException {
02   ObjectOutputStream objectOutputStream = null;
03   try {
04     RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
05     FileOutputStream fos = new FileOutputStream(raf.getFD());
06     objectOutputStream = new ObjectOutputStream(fos);
07     objectOutputStream.writeObject(test);
08   finally {
09     if (objectOutputStream != null) {
10       objectOutputStream.close();
11     }     
12 }

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

01 private static Kryo kryo = new Kryo(); // version 2.x
02  
03 public void testWriteBuffered(TestObject test, String fileName) throwsIOException {
04   Output output = null;
05   try {
06     RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
07     output = new Output(new FileOutputStream(raf.getFD()), MAX_BUFFER_SIZE);
08     kryo.writeObject(output, test);
09   finally {
10     if (output != null) {
11       output.close();
12     }
13   }
14 }

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

01 public void testWriteBuffered(TestObject test, String fileName) throwsIOException {
02   RandomAccessFile raf = null;
03   try {
04     MemoryBuffer memoryBuffer = new MemoryBuffer(MAX_BUFFER_SIZE);
05     raf = new RandomAccessFile(fileName, "rw");
06     test.write(memoryBuffer);
07     raf.write(memoryBuffer.getBuffer());
08   catch (IOException e) {
09     if (raf != null) {
10       raf.close();
11     }
12   }
13 }

TestObject写入方法如下:

01 public void write(MemoryBuffer unsafeBuffer) {
02   unsafeBuffer.putLong(longVariable);
03   unsafeBuffer.putLongArray(longArray);
04   // we support nulls
05   boolean objectExists = stringObject != null;
06   unsafeBuffer.putBoolean(objectExists);
07   if (objectExists) {
08     unsafeBuffer.putCharArray(stringObject.toCharArray());
09   }
10   objectExists = secondStringObject != null;
11   unsafeBuffer.putBoolean(objectExists);
12   if (objectExists) {
13     unsafeBuffer.putCharArray(secondStringObject.toCharArray());
14   }
15 }

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

01 public class MemoryBuffer {
02   // getting Unsafe by reflection
03   public static final Unsafe unsafe = UnsafeUtil.getUnsafe();
04  
05   private final byte[] buffer;
06  
07   private static final long byteArrayOffset = unsafe.arrayBaseOffset(byte[].class);
08   private static final long longArrayOffset = unsafe.arrayBaseOffset(long[].class);
09   /* other offsets */
10  
11   private static final int SIZE_OF_LONG = 8;
12   /* other sizes */
13  
14   private long pos = 0;
15  
16   public MemoryBuffer(int bufferSize) {
17     this.buffer = new byte[bufferSize];
18   }
19  
20   public final byte[] getBuffer() {
21     return buffer;
22   }
23  
24   public final void putLong(long value) {
25     unsafe.putLong(buffer, byteArrayOffset + pos, value);
26     pos += SIZE_OF_LONG;
27   }
28  
29   public final long getLong() {
30     long result = unsafe.getLong(buffer, byteArrayOffset + pos);
31     pos += SIZE_OF_LONG;
32     return result;
33   }
34  
35   public final void putLongArray(final long[] values) {
36     putInt(values.length);
37     long bytesToCopy = values.length << 3;
38     unsafe.copyMemory(values, longArrayOffset, buffer, byteArrayOffset + pos, bytesToCopy);
39     pos += bytesToCopy;
40   }
41  
42  
43   public final long[] getLongArray() {
44     int arraySize = getInt();
45     long[] values = new long[arraySize];
46     long bytesToCopy = values.length << 3;
47     unsafe.copyMemory(buffer, byteArrayOffset + pos, values, longArrayOffset, bytesToCopy);
48     pos += bytesToCopy;
49     return values;
50   }
51  
52   /* other methods */
53 }

几个小时的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
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值