清晰勝過聰明: 改進 flatbuffers-go[更新記憶體洩露與 GC]

tsingson發表於2020-04-09

0. 起因

使用 flatbuffers 已經有相當長的一段時間了.

在幾個商用專案中, flatbuffers 也因快速的反序列化而帶來效能上的不少提升.

flatbuffers 尤其適合傳輸小塊資料, 一次序列化, 多個地方進行反序列化.

但 go 的 flatbuffers 有一些小遺憾:

  1. go flatbuffers 功能支援, 滯後於 c++ 版, Go 程式碼庫也很久沒有更新了. 相比 c ++ , go 版本缺少一些功能. 如 vector of unions , 在 unions 中包含 struct / strings . ( 注: go 版本的 flatbuffers 在 unions 中只能包含 table )
  2. 缺少 verifier 驗證器 ( 這是我需要的)
  3. go flatbuffers 序列化的速度, 慢於 gogo protobuf. flatbuffers 序列化消耗的時間, 大約是 gogo protobuf 的兩倍.
  4. go flatbuffers 不支援 go module. 尤其是自動生成的 go 程式碼存在相互引用時的 import 並不友好.
  5. go flatbuffers 的序列化程式碼不太優雅, 不太符合 go 的習慣風格

在這樣情況下, 我起了改進 go flatbuffers 的念頭.

flatbuffers 的編譯器, 是 c++ 寫的. 我已經很多年沒有用過 c++ 開發了. 對我來說, 這可能是一次有趣的探險歷程.

1. 我對 go flatbuffers 的折騰

剛開始, 我寫一個 flatbuffers verifier , 本地驗證通過後, 我向 google flatbuffers 發了一個 PR. 結果被建議我重讀一下 flatbuffers 的設計規範文件. 嗯哼, 這就開始有趣了.

在接下來的兩週左右, 我邊讀 flatbuffers 的關鍵規範文件 ( 見附錄參考列表) , 邊寫了一個全新的序列化生成器 ( flatbuffers builder ) .

我拆分了 flatbuffers 的 memory block , 採用 goroutine 併發處理各個獨立的 memory block 轉化為二進位制序列資料, 最後進行合併/排序/優化. 當這個手寫序列化器看起來可以工作時, 我發現, 需要把這些手寫程式碼嵌入 flatbuffers 編譯器中, 支援自動程式碼生成, 我遇到了一個小難題. 我幾乎忘記如何寫 C++ 了.

為此, 我重讀了 Effective C++ 這樣的幾本冊子, 隨書寫幾行程式碼跑跑. 一週之後, 重新熟悉 C++ , 意外收穫是對 go 的記憶體管理有了進一步的認識.

如何讓 go flatbuffers 序列化更快, 我還在嘗試中.

而熟悉了 C++ 後, 我先讓 go flatbuffers API 變得清晰簡單, 易用一些.

2. 移植 C++ 有用功能, 支援 vector of unions.

union 是 flatbuffers 中很有趣也很有用的一個功能, 當然, struct 也很有用. go flatbuffers 中, union 只支援 table , 並且不支援 union array ( 被稱為 vector of unions ) , 先加上這個

IDL

union Character {
  MuLan: Attacker,  // table, 相當於 protobuf 中的 message
  Rapunzel,         // struct , 與 c++ 的 struct 相當
  Belle: BookReader,
  BookFan: BookReader,
  Other: string,   // string 
  Unused: string
}

table Movie {
  main_character: Character;      // 單一 union 欄位
  characters: [Character];            // vector of unions 
}

3. 支援 go module via Attribute ( 在 IDL 定義中 ).

每一個 fbs IDL 定義檔案都支援各自的 module , 格式像這樣: "go_module:github.com/tsingson/flatbuffers-sample/go-example/";

weapons.fbs

namespace weapons;

attribute "go_module:github.com/tsingson/flatbuffers-sample/samplesNew/";

table Gun {
  damage:short;
  bool:bool;
  name:string;
  names:[string];
}

monster.fbs

include "../weapons.fbs";

namespace Mygame.Example;

attribute "go_module:github.com/tsingson/flatbuffers-sample/go-example/";

enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment {   MuLan: Weapon, Weapon, Gun:weapons.Gun, SpaceShip,   Other: string } // Optionally add more tables.

......

生成的 go 程式碼

package Example

import (
    "strconv"
    flatbuffers "github.com/google/flatbuffers/go"

    weapons "github.com/tsingson/flatbuffers-sample/samplesNew/weapons"    /// 嗯哼!  
)

type Equipment byte

..........

4. 增加一些清晰易用的 API /生成程式碼.

weaponsOffset := flatbuffers.UOffsetT(0)
if t.Weapons != nil {
    weaponsLength := len(t.Weapons)
    weaponsOffsets := make([]flatbuffers.UOffsetT, weaponsLength)
    for j := weaponsLength - 1; j >= 0; j-- {
        weaponsOffsets[j] = t.Weapons[j].Pack(builder)
    }
    MonsterStartWeaponsVector(builder, weaponsLength)            //////// start
    for j := weaponsLength - 1; j >= 0; j-- {
        builder.PrependUOffsetT(weaponsOffsets[j])
    }
    weaponsOffset = MonsterEndWeaponsVector(builder, weaponsLength)   /////// end 
}

shortcut for [] strings vector

// native object 

    Names []string


// builder

namesOffset := builder.StringsVector( t.Names...)


getter for vector of unions


func (rcv *Movie) Characters(j int, obj *flatbuffers.Table) bool {
    o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
    if o != 0 {
        a := rcv._tab.Vector(o)
        obj.Pos = a + flatbuffers.UOffsetT(j*4)
        obj.Bytes = rcv._tab.Bytes
        return true
    }
    return false
}

so get struct or table

// GetStructVectorAsBookReader shortcut to access struct in vector of unions
func GetStructVectorAsBookReader(table *flatbuffers.Table) *BookReader {
    n := flatbuffers.GetUOffsetT(table.Bytes[table.Pos:])
    x := &BookReader{}
    x.Init(table.Bytes, n+ table.Pos)
    return x
}

// GetStructAsBookReader shortcut to access struct in single union field
func GetStructAsBookReader(table *flatbuffers.Table) *BookReader {
    x := &BookReader{}
    x.Init(table.Bytes, table.Pos)
    return x
}

for object-api , comments in generated code to make it clear


// UnPack use for single union field
 func (rcv Character) UnPack(table flatbuffers.Table) *CharacterT {
    switch rcv {
    case CharacterMuLan:
        x := GetTableAsAttacker(&table)
        return &CharacterT{ Type: CharacterMuLan, Value: x.UnPack() }
 .............

// UnPackVector use for vector of unions 
func (rcv Character) UnPackVector(table flatbuffers.Table) *CharacterT {
    switch rcv {
    case CharacterMuLan:
        x := GetTableVectorAsAttacker(&table)
        return &CharacterT{ Type: CharacterMuLan, Value: x.UnPack() }
    case CharacterRapunzel:
.........

或許, 稍後更多, 讓 Go flatbuffers ...... 更好用.

5. 關於記憶體洩露與 Go GC

C++ 程式碼在 CI 時提示記憶體洩露, 查了一整天..........

看 C++ 程式碼

// Save out the generated code for a Go Table type.
bool SaveType(const Definition &def, const std::string *classcode,
              const bool needs_imports, const bool is_enum) {
  if (!classcode->length()) return true;

  // fix  miss name space issue
 auto dns=  new Namespace();
  if ((parser_.root_struct_def_) &&
      (def.defined_namespace->components.empty())) {
    dns->components.push_back(parser_.root_struct_def_->name);
  } else {
    dns = def.defined_namespace;
  }

  Namespace &ns = go_namespace_.components.empty() ? *dns : go_namespace_;

auto dns= new Namespace(); -----------> 定義了一個指標變數, 並且初始化 在下面的 if 語句中使用了該指標變數, 但在 if else 程式碼塊中, dns 指標變數被指向另一個 Namespace 指標, 這樣在 if 語句中的指標變數成了野指標, 造成記憶體洩露

修改後程式碼如下, 注: 把指標使用程式碼移動到 if else 程式碼塊中, 在哪裡定義指標在哪裡使用

// Save out the generated code for a Go Table type.
bool SaveType(const Definition &def, const std::string *classcode,
              const bool needs_imports, const bool is_enum) {
  if (!classcode->length()) return true;

  // fix  miss name space issue

  if ((parser_.root_struct_def_) &&
      (def.defined_namespace->components.empty())) {
auto  dns = new Namespace();
    dns->components.push_back(parser_.root_struct_def_->name);
 Namespace &ns = go_namespace_.components.empty() ? *dns : go_namespace_;
  ..........
  } else {

 Namespace &ns = go_namespace_.components.empty() ? *def.defined_namespace : go_namespace_;
    ................
  }



在 go 中, 如果使用同樣的代友, 例如

type Namespace struct {
Components Stack;  // 這是一個FILO 的 stack 堆結構, 支援 Push / Pop 以及在  Pushback 及 Popback 在 stack 尾部新增元素或彈出元素
...
}

func SaveType ( def Definition, classcode *string , needs_imports, is _enum bool ) bool {

.......

dns = new Namespace;

if ( ................... ) {

dns. Components.Pushback( .........) 

} else {

 dns = def.DefinedNamespace; // 這是一個已經存在的 Namespace 指標
}



這樣的寫法, 其實與 C++ 一樣, 原來 new 產生的 dns 會有記憶體洩露的可能, 但這個 dns 在 Go 中由於已經沒有任何引用, 所以, 稍後被 go runtime 執行時進行 GC 了.

go 果然是 big C , 增強的 C++, GC 的存在讓開發過程輕鬆很多.

不過, 指標型別變數的引用, 可能引起記憶體洩露, 以及哪些情況可能被 GC , 哪些情況不會被 GC , 開發時得有個心數. 比如使用 go unsafe 時要和 C++ 一樣小心.

6. happy hacking....... 折騰繼續中

本文持續有更新...........

如果你對 flatbuffers 的 Go 版本有任何想法, 請簡單直接告訴我 ( 發郵件到 tsingson_at_me_com, 看看我能做些什麼有趣的...... 謝先.

祝安康愉快!

_

本站首發, 轉載請使用本頁連線網址, 謝

_

關於我

網名 tsingson (三明智)

原 ustarcom IPTV/OTT 事業部播控產品線技術架構溼/解決方案工程溼角色 (8 年), 自由職業者,

喜歡音樂 (口琴,是第三/四/五屆廣東國際口琴嘉年華的主策劃人之一), 攝影與越野,

喜歡 golang 語言 (商用專案中主要用 postgres + golang )

_

_

_ tsingson ( 三明智 ) 於深圳南山. 小羅號口琴音樂中心 2020/04/09

_

_

_

更多原創文章乾貨分享,請關注公眾號
  • 清晰勝過聰明: 改進 flatbuffers-go[更新記憶體洩露與 GC]
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章