0. 起因
使用 flatbuffers 已经有相当长的一段时间了.
在几个商用项目中, flatbuffers 也因快速的反序列化而带来性能上的不少提升.
flatbuffers 尤其适合传输小块数据, 一次序列化, 多个地方进行反序列化.
但 go 的 flatbuffers 有一些小遗憾:
- go flatbuffers 功能支持, 滞后于 c++ 版, Go 代码库也很久没有更新了. 相比 c ++ , go 版本缺少一些功能. 如 vector of unions , 在 unions 中包含 struct / strings . ( 注: go 版本的 flatbuffers 在 unions 中只能包含 table )
- 缺少 verifier 验证器 ( 这是我需要的)
- go flatbuffers 序列化的速度, 慢于 gogo protobuf. flatbuffers 序列化消耗的时间, 大约是 gogo protobuf 的两倍.
- go flatbuffers 不支持 go module. 尤其是自动生成的 go 代码存在相互引用时的 import 并不友好.
- 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....... 折腾继续中
本文持续有更新...........
.
.
本文首发于 GolangChina , 在此 gocn.vip/topics/1022…
祝安康愉快!
_
_
关于我
网名 tsingson (三明智)
原 ustarcom IPTV/OTT 事业部播控产品线技术架构湿/解决方案工程湿角色(8年), 自由职业者,
喜欢音乐(口琴,是第三/四/五届广东国际口琴嘉年华的主策划人之一), 摄影与越野,
喜欢 golang 语言 (商用项目中主要用 postgres + golang )
tsingson ( 三明智 ) 于深圳南山. 小罗号口琴音乐中心 2020/04/09
有疑问加站长微信联系(非本文作者)