Chinaunix首页 | 论坛 | 博客
  • 博客访问: 626878
  • 博文数量: 108
  • 博客积分: 46
  • 博客等级: 民兵
  • 技术积分: 1279
  • 用 户 组: 普通用户
  • 注册时间: 2012-08-16 11:36
个人简介

坐井以观天

文章分类

全部博文(108)

文章存档

2014年(13)

2013年(90)

2012年(6)

分类: C/C++

2013-05-23 21:06:11

当前项目中普遍用到GOOGLE 的一个开源大作PROTOBUF,把它作为网络应用层面的传输协议,至于它的诸多优势这里不作多说了!直接入正题!
前几日在PROTOBUF上面有严重的效率和空间开销的问题,没想到这两大难题一下子都来了,来得真是“迅雷不及掩耳之势”!跟踪后发现问题出在了“嵌套的MESSAGE个数过多,时间开销基本上全花在了ADD message上面”,例如:
message B{
标记 类型 变量 = 序列号;
...
}
message A{
repeated B f = 1;
}
A a;
这里message A中有上百万个message B,时间开销基本花在了 a.add_f()上面,而且空间上面开销也比较大。
针对这个头疼的问题,头这几天也在着急,项目都进行到这儿出了这等问题!之前我一直怀疑我们的策略问题,这几天在头儿的指示下,研究了一下PROTOBUF的源码,了解其中部分实现体制。这里简单地跟大家分享一下,大家尽管把砖头拍过来,嘿嘿!^_^
protobuf针对required 标记的字段分了两类,对每类都有相应的处理方式。其一:MESSAGE STRING;其二:非MESSAGE 和STRING,即原始数据类型,如INT32 INT64 FLOAT 等。我们把它们称为:repeated message(或者string)和repeated raw(原始数据类型)两种。PROTOBUF针对前者对内存的管理是,每次ADD时都会NEW一次;而后者先预分配了4个空间,随后成倍地动态增长空间(这个过程包括分配新空间,把原先的数据挪过来,再释放之前的旧空间三个操作);
虽然前都也有预先分配空间和动态增长空间,但分配的是用来存放MESSAGE或者STRING对象地址的空间,每次ADD的时候仍然要为数据分配空间。
下面把其中的一些实现细节贴出来,对比看一下:
repeated message(或者string)类的定义:

点击(此处)折叠或打开

  1. template <typename Element>
  2. class RepeatedPtrField : public internal::RepeatedPtrFieldBase {
  3.  public:
  4.   RepeatedPtrField();
  5.   RepeatedPtrField(const RepeatedPtrField& other);
  6.   ~RepeatedPtrField();

RepeatedPtrField继承了RepeatedPtrFieldBase类的一些比较重要的成员如下:

点击(此处)折叠或打开

 

  1. static const int kInitialSize = 4;

  2.   void** elements_;
  3.   int current_size_;
  4.   int allocated_size_;
  5.   int total_size_;

  6.   void* initial_space_[kInitialSize];

上面的几个成员的定义在RepeatedPtrFieldBase类里面。

repeated raw(原始数据类型)类的定义:

点击(此处)折叠或打开

  1. template <typename Element>
  2. class RepeatedField {
  3.  public:
  4.   RepeatedField();
  5.   RepeatedField(const RepeatedField& other);
  6.   ~RepeatedField();
  7. 。。。。

  8. 点击(此处)折叠或打开

    1. private:
    2.   static const int kInitialSize = 4;

    3.   Element* elements_;
    4.   int current_size_;
    5.   int total_size_;

    6.   Element initial_space_[kInitialSize];
    。。。

elements_就是据说的预分配数组,初始数组元素的个数是kInitialSize,就是4.
current_size_表示当前已经占用的元素个数
total_size_表示当前数组总大小
注意:这两个类中的成员 elements_的类型的区别,前者,用来存放的是message或者string对象空间的地址;后者用来存放的是真正的数据地址。

repeated message(或者string)对ADD的处理方式:

点击(此处)折叠或打开

  1. template <typename Element>
  2. inline Element* RepeatedPtrField<Element>::Add() {
  3.   return RepeatedPtrFieldBase::Add<TypeHandler>();
  4. }


点击(此处)折叠或打开

  1. template <typename TypeHandler>
  2. inline typename TypeHandler::Type* RepeatedPtrFieldBase::Add() {
  3.   if (current_size_ < allocated_size_) {
  4.     return cast<TypeHandler>(elements_[current_size_++]);
  5.   }
  6.   if (allocated_size_ == total_size_) Reserve(total_size_ + 1);
  7.   ++allocated_size_;
  8.   typename TypeHandler::Type* result = TypeHandler::New();
  9.   elements_[current_size_++] = result;
  10.   return result;
  11. }


点击(此处)折叠或打开

  1. void RepeatedPtrFieldBase::Reserve(int new_size) {
  2.   if (total_size_ >= new_size) return;

  3.   void** old_elements = elements_;
  4.   total_size_ = max(total_size_ * 2, new_size);
  5.   elements_ = new void*[total_size_];
  6.   memcpy(elements_, old_elements, allocated_size_ * sizeof(elements_[0]));
  7.   if (old_elements != initial_space_) {
  8.     delete [] old_elements;
  9.   }
  10. }
别看这里有动态增长内存空间,它这是做戏给人看的!它这里动态增长的是对象(习惯把message string称作“对象”,把原始类型称作“数据”,其实在C++眼里都是对象,只是本人癖性用C的眼光去看,^_^)地址空间,并不是真正的数据空间!

repeated raw(原始数据类型)对ADD的处理方式:

点击(此处)折叠或打开

  1. template <typename Element>
  2. inline Element* RepeatedField<Element>::Add() {
  3.   if (current_size_ == total_size_) Reserve(total_size_ + 1);
  4.   return &elements_[current_size_++];
  5. }


点击(此处)折叠或打开

  1. template <typename Element>
  2. void RepeatedField<Element>::Reserve(int new_size) {
  3.   if (total_size_ >= new_size) return;

  4.   Element* old_elements = elements_;
  5.   total_size_ = max(total_size_ * 2, new_size);
  6.   elements_ = new Element[total_size_];
  7.   MoveArray(elements_, old_elements, current_size_);
  8.   if (old_elements != initial_space_) {
  9.     delete [] old_elements;
  10.   }
  11. }

这里才是真正的动态增长数据空间。也就是说,只有第5、9、17、33。。。第N次%(2的M次方)==1时,才会重新去分配内存。
到这里,问题已经找到了,那么只能针对这里的两种特性来对我们的“应用协议”(PROTO文件中体现出来的应用架构)做一下协调、更改,可以避免repeated message(或者string)当repeated次数比较多的时候。!
我们相应地修改了一下PROTO文件,最终测试,发现时间上的开销果真缩小到了1/10左右,空间倒是没有减少多少,但多少还是少了一点儿!^_^ 唉!毕竟鱼和熊掌不可兼得,就时间和空间在软件这方面的地位而言,自古就是“难全”的,往往有一个要做出牺牲的!!!
其实去年就PROTOBUF的应用学习过一个月,还学习过一些比较高级的用法,但是并没有深入源码去理解它的实现机制,也没有去过多的去考虑它的性能问题!之后以为,PROTOBUF这个东西比较高级、比较好用,到现在知道再高级实用的东西也会有点瑕疵的,但瑕疵一般不会贴在脸上,还等着我们去发现,去适应它或者索引丢弃它不要!


阅读(30573) | 评论(4) | 转发(0) |
给主人留下些什么吧!~~

lurence2020-07-27 12:13:39

fuzhufang:请问你是怎么调整PROTO文件 来 减少这样的开销呢? 

百万级别用proto就是捡芝麻丢了西瓜,别说百万级别上千级别都不推荐,自己写一个二进制定制的比这个好多了

回复 | 举报

lurence2020-07-27 12:13:31

fuzhufang:请问你是怎么调整PROTO文件 来 减少这样的开销呢? 

百万级别用proto就是捡芝麻丢了西瓜,别说百万级别上千级别都不推荐,自己写一个二进制定制的比这个好多了

回复 | 举报

joepayne2014-10-08 19:05:58

fuzhufang:请问你是怎么调整PROTO文件 来 减少这样的开销呢? 

时间比较长了,之后就没做这方面的了,具体的格式记得不大清楚,用一个简单的例子说说吧:
message fields{
 integer col1 = 1,
 float col2 = 2,
 string col3 = 3
}
message records{
 repeated fields rec = 1  // add 10000次
}
像这种情形,可以把PROTO 文件调整成这样的:
message records{
 repeated integer col1 = 1, // add 10000 次 
 repeated float col2 = 2, // add 10000 次
 repeated&

回复 | 举报

fuzhufang2014-10-08 15:27:18

请问你是怎么调整PROTO文件 来 减少这样的开销呢?