三段序列化代码的测试:比较protocol buffers的CodedOutputStream和java自带的DataOutputStream
最近一段时间在写一个小东西,一个很简单k-v数据库。我并没有像MyISAM那样把每个表放在一个单独的文件中,而是用一个总的大文件来放所有的表。(类似于InnoDB默认的方式)。我需要在这个硕大无比的文件的开头放一个map,key是表名,value是这个表的第一个页在此文件中的偏移地址。即这样一个结构:Map < String, Long > headers。那么我就需要为这个Map写一个序列化方法,把它从Object转化成byte[]。写完第一个实现,并用junit测试完正确性之后,我准备再写2个实现,测试下性能。
三种实现的思路分别是:
1、用google protocol buffers的CodedOutputStream,手写序列化。先计算序列化之后需要多大空间,然后new出这个byte[],然后往里填。这是protoc生成的代码所采用的方式。
2、先new一个ByteArrayOutputStream,然后用它构造一个DataOutputStream,然后往里写,最后用ByteArrayOutputStream的toByteArray返回。其中字符串以UTF8的方式写入。
3、先new一个ByteArrayOutputStream,然后用它构造一个CodedOutputStream,手写序列化,最后用ByteArrayOutputStream的toByteArray返回。
ByteArrayOutputStream的默认buffer大小是32字节,如果DataOutputStream/CodedOutputStream往里面写的时候遇到它满了,就需要对现有的内存做一次copy来grow一下。这就是为什么我首先写的是方案一。但是方案一的缺点是,它需要把这个Map遍历2次。
测试环境:Core i3-2100,8GB内存,sun jdk 7。google protocol buffers的版本是2.4.1。
测试方式:首先往这个hashmap里面添1000条记录,key是长度为10的随机字符串,value是64位随机整数(0×0-0x7fffffffffffffffL之间均匀随机)。先warm up一下,然后执行1000次序列化方法。
测试结果:
方案1执行1000次花费时间=170ms-180ms左右。
方案2执行1000次花费时间=75ms-95ms左右。
方案3执行1000次花费时间=105ms-110ms左右。
序列化后的长度在20000到21000字节左右。google protocol buffers的最终码长看不出明显优势,甚至略差于DataOutputStream,这个比较符合我的推测,因为这种情况下,CodedOutputStream的变长编码方式发挥不出来优势。
方案1的代码:
public byte[] serialize(Object obj) throws IOException {
Map data=(Map)obj;
int size=CodedOutputStream.computeInt32SizeNoTag(data.size());
for(Map.Entry e:data.entrySet()){
size+=CodedOutputStream.computeStringSizeNoTag(e.getKey());
size+=CodedOutputStream.computeInt64SizeNoTag(e.getValue());
}
byte[] ret=new byte[size];
CodedOutputStream cos=CodedOutputStream.newInstance(ret);
cos.writeInt32NoTag(data.size());
for(Map.Entry e:data.entrySet()){
cos.writeStringNoTag(e.getKey());
cos.writeInt64NoTag(e.getValue());
}
//这句代码其实不必要
cos.flush();
return ret;
}
方案2的代码:
public byte[] serialize(Object obj) throws IOException {
Map data=(Map)obj;
ByteArrayOutputStream baos= new ByteArrayOutputStream();
DataOutputStream oos=new DataOutputStream(baos);
oos.write(data.size());
for(Map.Entry e:data.entrySet()){
oos.writeUTF(e.getKey());
oos.writeLong(e.getValue());
}
oos.flush();
return baos.toByteArray();
}
方案2的代码:
public byte[] serialize(Object obj) throws IOException {
Map data=(Map)obj;
ByteArrayOutputStream baos= new ByteArrayOutputStream();
CodedOutputStream cos=CodedOutputStream.newInstance(baos);
cos.writeInt32NoTag(data.size());
for(Map.Entry e:data.entrySet()){
cos.writeStringNoTag(e.getKey());
cos.writeInt64NoTag(e.getValue());
}
cos.flush();
return baos.toByteArray();
}
结论:Sometimes,Simple is the best。 这是一个很特殊的场景,所以测试结果说明不了什么问题,我只是因为最近看了一些关于如何做java code的benchmark的文章,实践一下那些方法。但是始终来说,Google protocol buffers对我最大的诱惑不是执行效率也不是最终码长,而是前后兼容。没有什么file format、protocol defines是一成不变的,对互联网产品,灵活比其它这2个都重要。