protobuf中會嚴重影響時間和空間損耗的地方

原文:http://blog.chinaunix.net/uid-26922071-id-3723751.html

當前項目中普遍用到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這個東西比較高級、比較好用,到現在知道再高級實用的東西也會有點瑕疵的,但瑕疵一般不會貼在臉上,還等着我們去發現,去適應它或者索引丟棄它不要!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章