最近项目中使用到了google protocol buffer作为数据传输格式,google protocol buffer(PB)相对于json来说序列化和反序列化速度都比较快。google protocol buffer的介绍详见:点击打开链接。
项目中的使用场景是存在一批数量比较多的数据pair,key为string,value为一个结构体,使用protocol buffer进行存储。使用的时候可预先将所有数据load到内存中,给的一个string类型的key后,需要快速的判断出是否能找到该key,如果找到需要返回该protocol buffer value的指针。
实现的时候,首先建立一个string类型key到size_t类型的正数的隐射关系,存储到double array trie(DA)结构中,这样可实现对key的较快查找,同时将所有的protocol buffer数据存储到一个大数组中,数组下标和DA的value值对应,这样如果找到对应的key,根据DA中的sizt_t类型的value快速得到PB类型的value。
1、数据制作
在实现数据存储时,将所有数据均以二进制的形式存储到文件中。
每一个PB对象调用SerializeToString方法将对象序列化为一个二进制string,将所有序列化后的二进制string写入文件的连续位置,我们还需要记录每一个string在文件中偏移位置以及string的长度存储在两个数组中在反序列化时使用。左后将偏移量和长度数组的内容也写入文件中。同时记录下每一个PB对象在string序列中的位置(从0开始),该位置作为DA结构中的value。
索引的制作代码如下:
bool HotelDoubleArray::buildFromFile(const string& info_file,const string& save_file,const string& md5_file)
{
//info_dic.clear();
ifstream fin(info_file.c_str());
if (!fin)
{
_INFO("ERROR: open file fail:%s",info_file.c_str());
return false;
}
FILE *fout;
fout = fopen(save_file.c_str(),"wb+");
if (!fout)
{
_INFO("ERROR: open file:%s]",save_file.c_str());
return false;
}
ofstream md5_fout(md5_file.c_str());
if(!md5_fout)
{
_INFO("[Open md5 file fail. file name = %s]",md5_file.c_str());
return false;
}
fseek(fout,0,SEEK_SET);
char tmpCh[1024 * 1024];
memset(tmpCh,'\0',sizeof(tmpCh));
vector<size_t> offset_vec;
vector<size_t> binary_length_vec;
size_t char_offset = 0;
size_t file_offset = 0;
fwrite(&char_offset,sizeof(size_t),1,fout);
file_offset += sizeof(size_t) * 1;
string line;
int lineNum = 0;
map<string,int> key_map;
vector<string> strVec;
while(getline(fin,line))
{
if (line.empty())
{
//cerr << "empty line\n";
continue;
}
strVec.clear();
tokenize(line,strVec,"\t");
if (strVec.size() != 21)
{
//cerr << "line " <<lineNum << " error: " << line << endl;
continue;
}
const string& key_str = strVec[0];
const string& room_id = strVec[1];
const string& real_source = strVec[2];
const string& room_type = strVec[3];
const int occupancy = atoi(strVec[4].c_str());
const string& bed_type = strVec[5];
int size = atoi(strVec[6].c_str());
int floor = atoi(strVec[7].c_str());
bool is_extrabed = false;
bool is_extrabed_free = false;
bool has_breakfast = false;
bool is_break_free = false;
bool is_cancel_free = false;
if("Yes" == strVec[8])
is_extrabed = true;
if("Yes" == strVec[9])
is_extrabed_free = true;
if("Yes" == strVec[10])
has_breakfast = true;
if("Yes" == strVec[11])
is_break_free = true;
if("Yes" == strVec[12])
is_cancel_free = true;
const string& room_desc = strVec[13];
const string& ori_room_type = strVec[14];
const string& norm_room_type = strVec[15];
const string& pay_method = strVec[16];
const string& extrabed_rule = strVec[17];
const string& return_rule = strVec[18];
const string& change_rule = strVec[19];
const string& others_info = strVec[20];
bool contain_dorm = false;
if (string::npos != room_type.find("宿舍")
|| string::npos != room_type.find("个床位")
|| string::npos != room_type.find("客房床位")
|| string::npos != bed_type.find("宿舍")
|| string::npos != room_desc.find("宿舍")
&& string::npos != room_desc.find("床位")
|| string::npos != room_type.find("dorm")
|| string::npos != room_type.find("Dorm")
|| string::npos != room_type.find("hostel")
|| string::npos != room_type.find("Hostel")
|| string::npos != room_type.find("8 bedded room"))
{
contain_dorm = true;
}
if (key_map.end() != key_map.find(key_str))
continue;
Hotel_Info hotel_info;
hotel_info.set_room_id(room_id);
hotel_info.set_real_source(real_source);
hotel_info.set_room_type(room_type);
hotel_info.set_bed_type(bed_type);
hotel_info.set_room_desc(room_desc);
hotel_info.set_ori_room_type(ori_room_type);
hotel_info.set_norm_room_type(norm_room_type);
hotel_info.set_pay_method(pay_method);
hotel_info.set_extrabed_rule(extrabed_rule);
hotel_info.set_return_rule(return_rule);
hotel_info.set_change_rule(change_rule);
hotel_info.set_others_info(others_info);
hotel_info.set_occupancy(occupancy);
hotel_info.set_size(size);
hotel_info.set_floor(floor);
hotel_info.set_is_extrabed(is_extrabed);
hotel_info.set_is_extrabed_free(is_extrabed_free);
hotel_info.set_has_breakfast(has_breakfast);
hotel_info.set_is_break_free(is_break_free);
hotel_info.set_is_cancel_free(is_cancel_free);
hotel_info.set_contain_dorm(contain_dorm);
cout << key_str
<< "\t" << room_id
<< "\t" << real_source
<< "\t" << room_type
<< "\t" << bed_type
<< "\t" << ori_room_type
<< "\t" << norm_room_type
<< "\t" << pay_method
<< "\t" << extrabed_rule
<< "\t" << return_rule
<< "\t" << change_rule
<< "\t" << others_info
<< "\t" << occupancy << endl;
string cand_info_str;
hotel_info.SerializeToString(&cand_info_str);
const size_t char_count = cand_info_str.length() + 1;
offset_vec.push_back(char_offset);
binary_length_vec.push_back(char_count);
char_offset += char_count;
fwrite(cand_info_str.c_str(),sizeof(char),char_count,fout);
key_map[key_str] = lineNum;
md5_fout << key_str << endl;
++lineNum;
}
fin.close();
fclose(fout);
fout = NULL;
file_offset += char_offset;
if(offset_vec.size() != binary_length_vec.size())
{
_INFO("[Error] [offset size != binary_length_vec size]");
return false;
}
fout = fopen(save_file.c_str(),"rb+");
if(!fout)
{
_INFO("[Open file fail. file = %s]",save_file.c_str());
return false;
}
fseek(fout,0,SEEK_SET);
_INFO("[char size: %ld offset:0]",char_offset);
fwrite(&char_offset,sizeof(size_t),1,fout);
fclose(fout);
fout = NULL;
fout = fopen(save_file.c_str(),"ab+");
if (!fopen)
{
_INFO("[Open file fail. file = %s]",save_file.c_str());
return false;
}
const size_t offset_size = offset_vec.size();
_INFO("[offset size:%ld offset:%ld]",offset_size,file_offset);
fwrite(&offset_size,sizeof(size_t),1,fout);
file_offset += sizeof(size_t) * 1;
//write offset info
_INFO("[offset_vec offset:%ld]",file_offset);
fwrite(&offset_vec[0],sizeof(size_t),offset_size,fout);
file_offset += sizeof(size_t) * offset_size;
_INFO("[binary length offset:%ld]",file_offset);
fwrite(&binary_length_vec[0],sizeof(size_t),offset_size,fout);
file_offset += sizeof(size_t) * offset_size;
size_t m_size = key_map.size();
char** m_key = new char*[m_size];
size_t* m_keylen = new size_t[m_size];
int* m_val = new int[m_size];
if(!(doubleArray && m_key && m_keylen && m_val))
{
_INFO("[Memory error]");
return false;
}
int count = 0;
for(std::map<string,int>::iterator it = key_map.begin();it != key_map.end();it++)
{
const string cand_key = it->first;
const int cand_value = it->second;
m_key[count] = new char[cand_key.length() + 1];
strcpy(m_key[count],cand_key.c_str());
m_keylen[count] = cand_key.length();
m_val[count] = cand_value;
count++;
}
if(count != m_size)
{
_INFO("[Error] [data count fail]");
return false;
}
int ret = doubleArray->build(m_size,m_key,m_keylen,m_val);
if(0 != ret)
{
_INFO("[Make info DA fail]");
return false;
}
size_t da_size = doubleArray->size() * doubleArray->unit_size();
fwrite(&da_size,sizeof(size_t),1,fout);
file_offset += sizeof(size_t) * 1;
_INFO("[DA size:%ld offset:%ld]",da_size,file_offset);
fclose(fout);
if(doubleArray->save(save_file.c_str(),"ab+",file_offset) < 0)
{
_INFO("[Error] [save file fail]");
return false;
}
if(m_val)
{
delete[] m_val;
}
if(m_keylen)
{
delete m_key;
}
if(m_key)
{
for(size_t k = 0;k < m_size;k++)
{
if(m_key[k])
delete[] m_key[k];
}
delete[] m_key;
}
return true;
}
2、数据的反序列化
对数据进行发序列化时,首先将所有二进制string从文件中一次性load到内存中,然后将string的长度以及偏移量信息load到内存中,剩下的工作就是对二进制string进行反序列化,得到PB对象。
因为数据量比较大,并且所有数据均是在内存中进行操作,不存在IO限制,所以加快反序列化最直接的方法就是利用多线程,每个线程单独的对一部分数据进行反序列化操作,各线程之间不存在通信和资源共享操作,理论上来说应该可以大大加快反序列化速度。
反序列化所线程代码:
const int max_thread_count = 6;
struct ParsePara
{
char* str_buffer;
size_t* buffer_offset;
size_t* binary_string_length_array;
size_t start_idx;
size_t end_idx;
Hotel_Info* info_buffer_array;
};
void parseHotelInfoForMany(const char* str_buffer,Hotel_Info* info_buffer_array,const size_t* buffer_offset,
const size_t start_idx,const size_t end_idx,size_t* binary_string_length_array)
{
for(size_t k = start_idx;k < end_idx;k++)
{
const size_t cand_offset = buffer_offset[k];
const size_t binary_length = binary_string_length_array[k];
info_buffer_array[k].ParseFromArray(&str_buffer[cand_offset],binary_string_length_array[k] - 1);
}
}
void* parseHotelInfoForMultiThread(void* void_para_ptr)
{
ParsePara* para_ptr = (ParsePara*)void_para_ptr;
parseHotelInfoForMany(para_ptr->str_buffer,para_ptr->info_buffer_array,para_ptr->buffer_offset,
para_ptr->start_idx,para_ptr->end_idx,para_ptr->binary_string_length_array);
}
bool HotelDoubleArray::load(const string& file_name)
{
FILE* fin;
fin = fopen(file_name.c_str(),"rb");
if(!fin)
{
_INFO("[Open file fail]");
return false;
}
fseek(fin,0,SEEK_SET);
size_t file_offset = 0;
size_t char_size;
fread(&char_size,sizeof(size_t),1,fin);
_INFO("[char size:%ld 0]",char_size);
cout << "1 " << char_size << endl;
file_offset += sizeof(size_t) * 1;
cout << "2 " << char_size << endl;
cout << file_offset << endl;
cout << "3 " << char_size << endl;
char* m_valdata = new char[char_size];
cout << "4 " << char_size << endl;
if(!m_valdata)
{
_INFO("[get memory fail]");
return false;
}
fread(m_valdata,sizeof(char),char_size,fin);
cout << "5 " << char_size << endl;
file_offset += sizeof(char) * char_size;
cout << char_size << endl;
cout << sizeof(char) << endl;
cout << sizeof(char) * char_size << endl;
cout << file_offset << endl;
size_t offset_size;
fread(&offset_size,sizeof(size_t),1,fin);
_INFO("[offset size:%ld offset:%ld]",offset_size,file_offset);
file_offset += sizeof(size_t) * 1;
size_t* offset_array = new size_t[offset_size];
size_t* binary_length_array = new size_t[offset_size];
if(!offset_array || !binary_length_array)
{
_INFO("[Get memory fail]");
return false;
}
fread(offset_array,sizeof(size_t),offset_size,fin);
file_offset += sizeof(size_t) * offset_size;
fread(binary_length_array,sizeof(size_t),offset_size,fin);
file_offset += sizeof(size_t) * offset_size;
size_t da_size;
fread(&da_size,sizeof(size_t),1,fin);
fclose(fin);
file_offset += sizeof(size_t) * 1;
_INFO("[DA size:%ld offset:%ld]",da_size,file_offset);
if(doubleArray->open(file_name.c_str(),"rb",file_offset,da_size) < 0)
{
_INFO("[Load DA fail]");
return false;
}
info_array = new Hotel_Info[offset_size];
if(!info_array)
{
_INFO("[Malloc memory fail]");
return false;
}
pthread_t pthread_id_vec[max_thread_count];
vector<ParsePara*> para_vec;
const size_t each_count = ceil(float(offset_size) / max_thread_count);
for (size_t k = 0;k < max_thread_count;k++)
{
size_t start_idx = each_count * k;
size_t end_idx = each_count * (k+1);
if (start_idx >= offset_size)
break;
if (end_idx >= offset_size)
end_idx = offset_size;
ParsePara* cand_para_ptr = new ParsePara();
if (!cand_para_ptr)
{
_ERROR_EXIT(0,"[Malloc memory fail.]");
}
cand_para_ptr->str_buffer = m_valdata;
cand_para_ptr->buffer_offset = offset_array;
cand_para_ptr->start_idx = start_idx;
cand_para_ptr->end_idx = end_idx;
cand_para_ptr->info_buffer_array = info_array;
cand_para_ptr->binary_string_length_array = binary_length_array;
para_vec.push_back(cand_para_ptr);
}
for(size_t k = 0;k < para_vec.size();k++)
{
int ret = pthread_create(&pthread_id_vec[k],NULL,parseHotelInfoForMultiThread,para_vec[k]);
if(0 != ret)
{
_ERROR_EXIT(0,"[Create thread fail]");
}
}
for(int k = 0;k < para_vec.size();k++)
{
pthread_join(pthread_id_vec[k],NULL);
}
if(m_valdata)
{
delete[] m_valdata;
}
if(m_valdata)
{
delete[] m_valdata;
}
if(offset_array)
{
delete[] offset_array;
}
if(binary_length_array)
{
delete[] binary_length_array;
}
for(size_t k = 0;k < para_vec.size();k++)
{
if(para_vec[k])
{
delete para_vec[k];
}
}
return true;
}
代码逻辑比较简单,实现起来也很快。感觉任务就这么愉快地结束了。
当然需要比较一下多线程提高了多大的性能。见证奇迹的时刻就要到了么,然而并不是:
相同的模型文件、单线程耗时37124995us;开启6个线程(机器为8core)多线程耗时128721575us。妈蛋,多线程变慢了。
一定是我打开的方式不对,运行多线程发现在反序列化时,只有1到2个CPU处于运行状态,剩余的CPU总是处于等待状态,这说明各线程之间存在某种资源竞争,但是代码里各线程是完全独立的啊,而且protocol buffer文档也说了不存在任何的lock操作。
遇到问题了,剩下的就是解决问题。各种查资料,完全木有有帮助的东东,之后求组万能的Stack Overflow,问题链接点击打开链接,第二天上去一看,有人回复了。竟然是google protocol buffer 2.0版本的主要作者Kenton Varda。越是大牛越是没架子啊,膜拜。在回复中给我列了3个可能的原因,分析以后觉得只能是其中一个原因:PB对象只有在实际真正的使用时才会分配内存,也就是所new一个很大的PB数组,其实做的工作很少,只有在调用ParseFromArray的时候才会对对象进行内存的分配。而所有数据都是使用new进行分配的,new操作符实际上是调用malloc函数,而malloc函数不是thread-freiendly的,各线程之间存在锁操作。
问题到这明了了,就是因为线程使用调用malloc到这多线程之间互相等待。
3、解决方法
Kenton Varda给出的建议是使用google的tcmalloc代替系统默认的malloc。
3.1 tcmalloc安装:
直接从google官网下载,官网链接:点击打开链接,然后解压、编译。第一次编译出现错误,查了以下资料,configure的时候需要加上一个参数:
./configure --enable-frame-pointers
make && make install
然后在使用索引的Makefile.am的libHotelDoubleArray_la_LIBADD中加上“/usr/local/lib/libtcmalloc_minimal.la”,原来的代码不做任何修改,在操作内存的时候就会使用tcmalloc代替malloc。
使用相同的数据从新测试,开6个线程,耗时9400488us,时间明显加快了,达到了多线程的目的。