一、背景及概要
序列化,是指将数据以一种特定的格式转化为方便计算机经网络传输或储存的格式。
反序列化,以便在另一台计算机或者本机上以序列化格式重新得到数据的操作。
例如:
uint32_t number = 10;
在计算机中实际储存为32位数据:(高)00000000 00000000 00000000 00001010(低)
为4个Byte数据,因此可以将4个Bytes放入char类型(同样为8bits)的容器string当中,以完成序列化。再将string中的数据取出解释为uint32_t完成反序列化。
基于此思想,Jeff Dean在Building Software Systems at Google andLessons Learned[1]中提出了VBC编码以高效地完成uint32_t的序列化与反序列化。本实验完成了VBC编码的基本实现和测试速度。
但由于VBC编码的decode方式必须忍受分支预测的开销,因此Jeff Dean提出了一种新的编码方式Group Varint Encoding,来解决分支预测的问题。
二、实验方案
实验设备
实验概述
1、VBC编码
本实验先将按照如下思路进行了C++程序的编写并完成单元测试
encode 单个uint32_t
string encode_number(uint32_t n){
char current_byte;
string byte;
while (1) {
current_byte = n & 0x7f;
byte = current_byte + byte;
if (n < 128) break;
n = n >> 7;
}
byte[byte.size()-1] += 128;
return byte;
}
将vector中所有uint32_t序列化为一个string
string encode(const vector& in_sequence){
string serialized_integers;
const int size = in_sequence.size();
for (int i = 0; i < size; i++) {
serialized_integers += encode_number(in_sequence[i]);
}
return serialized_integers;
}
反序列化,将string中数据decode添加到vector
vector decode(const string& serialized_integers){
vector numbers;
uint32_t current_number = 0;
int index = 0;
while (index < serialized_integers.length()) {
unsigned char current_byte = serialized_integers[index];
uint32_t part = (uint32_t)current_byte;
//对于每一个字节都要判断当前字节是否为uint32_t的最后一个字节,分支预测开销大
if (part < 128) {
current_number = (current_number << 7) + part;
}else {
current_number = (current_number << 7) + (part - 128);
numbers.push_back(current_number);
current_number = 0;
}
index++;
}
return numbers;
}
代码分析
对于encode部分和decode部分,variant byte coding的方法均在每一个Byte上判断是否属于是当前整数的最后一个字节,因此在分支预测方面的开销大,每一次分支预测的需要5ns左右的时间,因此提出以下方法Group Variant Encoding。
2、Group Variant Encoding编码
编码思想:将每一个uint32_t用2个bits来表示该整数需要几个Byte来表示,一个uint32_t为4个字节正好需要两个bits来表示。
二进制 需要的字节数
00 1
00 2
00 3
00 4
一个uint32_t正好需要四个状态来表示
encode 一组uint32_t(数量<=4)
string encode_single_group(const vector& integers){
string encoded;
//该组的tag
char tags = 0;
size_t offset = 6;
//对于每一个数据进行encode
for(int i = 0; i < integers.size(); i++){
uint32_t number = integers[i];
//每个数据有一个子标签count记录需要多少个Byte
char count = 0x0;
//为0则不需要取,直接添加至encoded
if(number != 0){
char current_byte = 0xFF;
//当还有字节可以编码时:取出字节-》计数增加-》加入编码
while(number != 0){
current_byte = current_byte & number;
count++;
encoded += current_byte;
number = number >> 8;
current_byte = 0xFF;
}
tags = tags | (count-1 << offset);
}else{
encoded += (char)0x00;
tags = tags | (count << offset);
}
offset -= 2;
}
return (tags + encoded);
}
encode接口:将vector中所有uint32_t序列化为string
string group_varint_encode(const vector &original_integers){
if(original_integers.empty()) return "";
string encoded;
//分组器
int group_count = 0;
vector group;
for(int i = 0; i < original_integers.size(); i++){
group_count++;
group.push_back(original_integers[i]);
//若能填满四个数据则编码
if((group_count & 3) == 0){
encoded += encode_single_group(group);
group_count = 0;
group.clear();
}
}
//若数据数量不为4的倍数则再单独编码
if(group_count != 0) encoded += encode_single_group(group);
return encoded;
}
decode接口:反序列化string为vector
vector group_varint_decode(const string& encoded_byte_stream){
if(encoded_byte_stream.empty()) return {};
vector decoded;
const int len = encoded_byte_stream.length();
int index = 0;
while(index < len{
//取当前tag
char tags = encoded_byte_stream[index++];
int offset = 6;
//对当前组数据进行decode(tag后的5~17个Bytes)
//这里对每个字节有条件判断
while(offset >= 0 && index < len){
//follow为组内的其中一个数据需要的字节数
int follow = ((tags >> offset) & 3) + 1;
uint32_t value = 0;
//向后连续取follow字节
for(int i = 0; i < follow; i++){
unsigned char* trans = (unsigned char*)&encoded_byte_stream[index++];
value = value | ((uint32_t)*trans << i*8);
}
decoded.push_back(value);
offset -= 2;
}
}
return decoded;
}
代码分析
在该实现当中注意decode的以下部分
int offset = 6;
//对当前组数据进行decode(tag后的5~17个Bytes)
//这里对每个字节有条件判断
while(offset >= 0 && index < len)
如果这么编写代码,造成的后果是,并没有达到Group Varint Encoding的核心目的-------减小分支预测的开销,为了验证该设想,运用perf分析工具来进行观察。
#######观察
#perf record生成一份性能报告
#-e + 性能指标可以对某一具体指标进行测量
#-g 生成具体到每个函数的性能
#最后是可执行文件
#perf report 打开性能测试报告
perf record -e branch-misses -g ./a.out
perf report
可以看到甚至在此实现中,GVE的decode方法的branch-miss的占比甚至比VBC方法还高出了5%,因此需要对上述循环进行改进。
改进思路
改进思路无非在于,在运用tags时每取2bits都要判断tags是否结束,即是否该组数据处理完成。
那么可以换一种处理方法,在处理前每一次都判断该tags是否是最后一组tags。
如果对于四个一组的数据,顺序的连续取四次,不需要判断。
而对于最后一组数据数量在1 <= number <= 4,则需要每取一次判断是否越界。
性能预测
如果处理的数据足够大,那么分支预测的开销相较于之前,减小了3/4。
改进的decode接口
uint32_t get_value(const string& encoded_byte_stream, const vector& masks, size_t current_tag, uint32_t* address, int& index){
uint32_t value = 0;
address = (uint32_t*)&encoded_byte_stream[index];
value = *address & masks[current_tag];
index += current_tag + 1;
return value;
}
vector group_varint_decode(const string& encoded_byte_stream){
if(encoded_byte_stream.empty()) return {};
vector decoded;
const int len = encoded_byte_stream.length();
int index = 0;
const vector masks = {0xff, 0xffff, 0xffffff, 0xffffffff};
while(index < len){
//取当前tag
char tags = encoded_byte_stream[index++];
int number = (tags & 3) + ((tags >> 2) & 3) + ((tags >> 4) & 3) + ((tags >> 6) & 3) + 4;
uint32_t* address = (uint32_t*)&encoded_byte_stream[index];
size_t current_tag = (tags >> 6) & 3;
//最后一个
if((index + number) >= len){
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
if(index >= len) break;
current_tag = (tags >> 4) & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
if(index >= len) break;
current_tag = (tags >> 2) & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
if(index >= len) break;
current_tag = tags & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
if(index >= len) break;
}else{
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
current_tag = (tags >> 4) & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
current_tag = (tags >> 2) & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
current_tag = tags & 3;
decoded.push_back(get_value(encoded_byte_stream,masks, current_tag, address, index));
}
}
return decoded;
}
三、性能对比
encode对比
图像分析:
为两组对照,首先,可以看到,无论是否开启编译优化O3,GVE方法编码速度始终高于VBC编码。
其次,若开启编译优化O3,GVE方法的性能得到大幅度提升,提升了将近4倍,而VBC编码的速度也提升了将近7倍,但还是低于未进行编译优化的GVE编码。
结论:GVE的encode方法在速度上明显优于VBC的encode方法。
decode对比
图像分析:
从上到下依次为(图例起名可能引起混淆):
VBC 未编译优化
GVE 未编译优化 未改进
VBC O3编译优化
GVE O3编译优化 未改进
GVE 未编译优化 改进后
GVE O3编译优化 改进后
首先,无论对于GVE/VBC(未编译优化)还是 GVE/VBC(O3优化),两种方法性能其实相差不远,均遭受了分支预测所带来的开销,表现为曲线贴合紧密。
其次,关注标出数据的两条,黄色和绿色, 表示,在进行改进之后,GVE的性能得到了大幅度的提升,绿色的改进后GVE(O3优化)的速度远远超过其他版本的曲线,带来了性能上的优势。
结论:改进编写方式后的GVE的decode体现了Jeff Dean提出GVE编码方法的初衷,即避免遭遇分支预测所带来的性能瓶颈。
四、Conclusion And Future Work
本次实验运用两种序列化uint32_t的方法VBC和GVE, 进行了初步的性能测试,以及体会了编码方式的不同可以在性能上得到了巨大的提升,以及初步使用了perf性能分析工具和数据可视化的方法来解决问题。
future work方面,可以看到在最后一张decode性能对比图中,当数据数量上升时,GVE的decode方法遭遇了性能的严重下滑,因此,需要找出为什么会出现这样的断层现象,以及如果数据进一步增大,该方法性能的体现。找到性能瓶颈并尝试解决该现象,达到既高效又稳定的目标。
引用:
[1]:https://static.googleusercontent.com/media/research.google.com/zh-CN//people/jeff/Stanford-DL-Nov-2010.pdf
[2]:http://www.ir.uwaterloo.ca/book/addenda-06-index-compression.html