三段序列化代码的测试:比较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位随机整数(0x0-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<String, Long> data=(Map<String, Long>)obj;
        int size=CodedOutputStream.computeInt32SizeNoTag(data.size());
        for(Map.Entry<String, Long> 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<String, Long> e:data.entrySet()){
            cos.writeStringNoTag(e.getKey());
            cos.writeInt64NoTag(e.getValue());            
        }

        //这句代码其实不必要
        cos.flush();

        return ret;
    }

方案2的代码:

public byte[] serialize(Object obj) throws IOException {
        Map<String, Long> data=(Map<String, Long>)obj;
        ByteArrayOutputStream baos= new ByteArrayOutputStream();
        DataOutputStream oos=new DataOutputStream(baos); 
        oos.write(data.size());
        for(Map.Entry<String, Long> e:data.entrySet()){            
            oos.writeUTF(e.getKey());
            oos.writeLong(e.getValue());
        }

        oos.flush();        
        return baos.toByteArray();
    }

方案3的代码:

    public byte[] serialize(Object obj) throws IOException {
        Map<String, Long> data=(Map<String, Long>)obj;
        ByteArrayOutputStream baos= new ByteArrayOutputStream();
        CodedOutputStream cos=CodedOutputStream.newInstance(baos);
        cos.writeInt32NoTag(data.size());
        for(Map.Entry<String, Long> 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个都重要。

此博客中的热门博文

少写代码,多读别人写的代码

在windows下使用llvm+clang

tensorflow distributed runtime初窥