近期的一个项目有对结构化数据进行序列化和反序列化的需求,该项目具有performance critical属性,因此我们在选择序列化库包时是要考虑包的性能的。

github上有一个有关Go序列化方法性能比较的repo:go_serialization_benchmarks,这个repo横向比较了数十种数据序列化方法的正确性、性能、内存分配等,并给出了一个结论:推荐gogo protobuf。对于这样一个粗选的结果,我们是直接笑纳的^_^。接下来就是进一步对gogo protobuf做进一步探究。

一. go protobuf v1 vs. gogo protobuf

gogo protobuf是既go protobuf官方api之外的另一个go protobuf的api实现,它兼容go官方protobuf api(更准确的说是v1版本)。gogo protobuf提供了三种代码生成方式:protoc-gen-gogofast、protoc-gen-gogofaster和protoc-gen-gogoslick。究竟选择哪一个呢?这里我也写了一些benchmark来比较,并顺便将官方go protobuf api也一并加入比较了。

我们首先安装一下gogo protobuf实现的protoc的三个插件,用于生成proto文件对应的Go包源码文件:

go get github.com/gogo/protobuf/protoc-gen-gofast
go get github.com/gogo/protobuf/protoc-gen-gogofastergo get github.com/gogo/protobuf/protoc-gen-gogoslick

安装后,我们在$GOPATH/bin下将看到这三个文件(protoc-gen-go是go protobuf官方实现的代码生成插件):

$ls -l $GOPATH/bin|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*

为了对采用不同插件生成的数据序列化和反序列化方法进行性能基准测试,我们建立了下面repo。在repo中,每一种方法生成的代码放入独立的module中:

$tree -L 2 -F
.
├── IDL/
│   └── submit.proto
├── Makefile
├── gogoprotobuf-fast/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-faster/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
├── gogoprotobuf-slick/
│   ├── go.mod
│   ├── go.sum
│   ├── submit/
│   └── submit_test.go
└── goprotobuf/├── go.mod├── go.sum├── submit/└── submit_test.go

我们的proto文件如下:

$cat IDL/submit.proto
syntax = "proto3";option go_package = ".;submit";package submit;message request {int64 recvtime = 1;string uniqueid = 2;string token = 3;string phone = 4;string content = 5;string sign = 6;string type = 7;string extend = 8;string version = 9;
}

我们还建立了Makefile,用于简化操作:

$cat Makefilegen-protobuf: gen-goprotobuf gen-gogoprotobuf-fast gen-gogoprotobuf-faster gen-gogoprotobuf-slickgen-goprotobuf:protoc -I ./IDL submit.proto --go_out=./goprotobuf/submitgen-gogoprotobuf-fast:protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submitgen-gogoprotobuf-faster:protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submitgen-gogoprotobuf-slick:protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submitbenchmark: goprotobuf-bench gogoprotobuf-fast-bench gogoprotobuf-faster-bench  gogoprotobuf-slick-benchgoprotobuf-bench:cd goprotobuf && go test -bench .gogoprotobuf-fast-bench:cd gogoprotobuf-fast && go test -bench .gogoprotobuf-faster-bench:cd gogoprotobuf-faster && go test -bench .gogoprotobuf-slick-bench:cd gogoprotobuf-slick && go test -bench .

针对每一种方法,我们建立一个benchmark test。benchmark test代码都是一样的,我们以gogoprotobuf-fast为例:

// submit_test.gopackage protobufbenchimport ("fmt""os""testing""github.com/bigwhite/protobufbench_gogoprotofast/submit""github.com/gogo/protobuf/proto"
)var request = submit.Request{Recvtime: 170123456,Uniqueid: "a1b2c3d4e5f6g7h8i9",Token:    "xxxx-1111-yyyy-2222-zzzz-3333",Phone:    "13900010002",Content:  "Customizing the fields of the messages to be the fields that you actually want to use removes the need to copy between the structs you use and structs you use to serialize. gogoprotobuf also offers more serialization formats and generation of tests and even more methods.",Sign:     "tonybaiXZYDFDS",Type:     "submit",Extend:   "",Version:  "v1.0.0",
}var requestToUnMarshal []bytefunc init() {var err errorrequestToUnMarshal, err = proto.Marshal(&request)if err != nil {fmt.Printf("marshal err:%s\n", err)os.Exit(1)}
}func BenchmarkMarshal(b *testing.B) {b.ReportAllocs()for i := 0; i < b.N; i++ {_, _ = proto.Marshal(&request)}
}
func BenchmarkUnmarshal(b *testing.B) {b.ReportAllocs()var request submit.Requestfor i := 0; i < b.N; i++ {_ = proto.Unmarshal(requestToUnMarshal, &request)}
}func BenchmarkMarshalInParalell(b *testing.B) {b.ReportAllocs()b.RunParallel(func(pb *testing.PB) {for pb.Next() {_, _ = proto.Marshal(&request)}})
}
func BenchmarkUnmarshalParalell(b *testing.B) {b.ReportAllocs()var request submit.Requestb.RunParallel(func(pb *testing.PB) {for pb.Next() {_ = proto.Unmarshal(requestToUnMarshal, &request)}})
}

我们看到,对每种方法生成的代码,我们都会进行顺序和并行的marshal和unmarshal基准测试。

我们首先分别使用不同方式生成对应的go代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

然后运行基准测试(使用macos上的go 1.14):

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2437068           483 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2262229           529 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7592120           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        5306744           225 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.239s
cd gogoprotobuf-fast && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofast
BenchmarkMarshal-8                  7186828           164 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4706794           251 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15107896            83.0 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6258507           179 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofast    5.449s
cd gogoprotobuf-faster && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotofaster
BenchmarkMarshal-8                  7036842           166 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4666698           256 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15444961            83.2 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6936337           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotofaster    5.750s
cd gogoprotobuf-slick && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_gogoprotoslick
BenchmarkMarshal-8                  6529311           176 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                4737463           252 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8       15700746            81.8 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        6528390           202 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_gogoprotoslick    5.668s

在我的macpro(4核8线程)上,我们看到两点结论:

•官方go protobuf实现生成的代码性能的确弱于gogo protobuf生成的代码,在顺序测试中,差距还较大;•针对我预置的proto文件中数据格式,gogo protobuf的三种生成方法产生的代码的性能差异并不大,选择protoc-gen-gofast生成的代码在性能上即可满足。

二. go protobuf v2

今年三月份初,Go官方发布了protobuf的新API版本,这个版本与原go protobuf并不兼容。新版API旨在使protobuf的类型系统与go类型系统充分融合,提供反射功能和自定义消息实现。那么该版本生成的序列/反序列化代码在性能上有提升吗?我们将其加入我们的benchmark。

我们先下载go protobuf v2的代码生成插件(注意:由于go protobuf v1和go protobuf v2的插件名称相同,需要先备份好原先已经安装的protoc-gen-go):

$  go get google.golang.org/protobuf/cmd/protoc-gen-go
go: found google.golang.org/protobuf/cmd/protoc-gen-go in google.golang.org/protobuf v1.21.0

然后将新安装的插件名称改为protoc-gen-gov2,这样$GOPATH/bin下的插件文件列表如下:

$ls -l $GOPATH/bin/|grep proto
-rwxr-xr-x   1 tonybai  staff   6252344  4 24 14:43 protoc-gen-go*
-rwxr-xr-x   1 tonybai  staff   9371384  2 28 09:35 protoc-gen-gofast*
-rwxr-xr-x   1 tonybai  staff   9376152  2 28 09:40 protoc-gen-gogofaster*
-rwxr-xr-x   1 tonybai  staff   9380728  2 28 09:40 protoc-gen-gogoslick*
-rwxr-xr-x   1 tonybai  staff   8716064  4 24 14:56 protoc-gen-gov2*

在Makefile中增加针对go protobuf v2的代码生成和Benchmark target:

gen-goprotobufv2:protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submitgoprotobufv2-bench:cd goprotobufv2 && go test -bench .

由于go protobuf v2与v1版本不兼容,因此也无法与gogo protobuf兼容,我们需要修改一下go protobuf v2对应的submit_test.go,将导入的"github.com/gogo/protobuf/proto"包换为"google.golang.org/protobuf/proto"

重新生成代码:

$make gen-protobuf
protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit
protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit
protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit
protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit
protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit

运行benchmark:

$make benchmark
cd goprotobuf && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goproto
BenchmarkMarshal-8                  2420620           485 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                2186240           538 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7334412           162 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4537429           222 ns/op         400 B/op           7 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goproto    6.052s
cd goprotobufv2 && go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2404473           506 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1901947           626 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        6629139           171 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8       panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11d4956]goroutine 196 [running]:
google.golang.org/protobuf/internal/impl.(*messageState).protoUnwrap(0xc00007e210, 0xc000010360, 0xc00008ce01)/Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:27 +0x26
google.golang.org/protobuf/internal/impl.(*messageState).Interface(0xc00007e210, 0xc00007e210, 0xc00012c000)/Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:24 +0x2b
google.golang.org/protobuf/proto.UnmarshalOptions.unmarshal(0x0, 0x12acc00, 0xc000010360, 0xc00012c000, 0x177, 0x177, 0x12b23e0, 0xc00007e210, 0xc000200001, 0x0, ...)/Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:71 +0x2c5
google.golang.org/protobuf/proto.Unmarshal(0xc00012c000, 0x177, 0x177, 0x12ac180, 0xc00007e210, 0x0, 0x0)/Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:48 +0x89
github.com/bigwhite/protobufbench_goprotov2.BenchmarkUnmarshalParalell.func1(0xc0004a8000)/Users/tonybai/test/go/protobuf/goprotobufv2/submit_test.go:65 +0x6a
testing.(*B).RunParallel.func1(0xc0000161b0, 0xc0000161a8, 0xc0000161a0, 0xc00010c700, 0xc00004a000)/Users/tonybai/.bin/go1.14/src/testing/benchmark.go:763 +0x99
created by testing.(*B).RunParallel/Users/tonybai/.bin/go1.14/src/testing/benchmark.go:756 +0x192
exit status 2
FAIL    github.com/bigwhite/protobufbench_goprotov2    4.878s
make: *** [goprotobufv2-bench] Error 1

我们看到go protobuf v2并未完成所有benchmark test,在运行并行unmarshal测试中panic了。目前go protobuf v2官方并未在github开通issue,因此尚不知道哪里去提issue。于是回到test代码,再仔细看一下submit_test.go中 BenchmarkUnmarshalParalell的代码:

func BenchmarkUnmarshalParalell(b *testing.B) {b.ReportAllocs()var request submit.Requestb.RunParallel(func(pb *testing.PB) {for pb.Next() {_ = proto.Unmarshal(requestToUnMarshal, &request)}})
}

这里存在一个“问题”,那就是多goroutine会共享一个request。但在其他几个测试中同样的代码并未引发panic。我修改一下代码,将其放入for循环中:

func BenchmarkUnmarshalParalell(b *testing.B) {b.ReportAllocs()b.RunParallel(func(pb *testing.PB) {for pb.Next() {var request submit.Request_ = proto.Unmarshal(requestToUnMarshal, &request)}})
}

再运行go protobuf v2的benchmark:

$go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/bigwhite/protobufbench_goprotov2
BenchmarkMarshal-8                  2348630           509 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshal-8                1913904           627 ns/op         400 B/op           7 allocs/op
BenchmarkMarshalInParalell-8        7133936           175 ns/op         384 B/op           1 allocs/op
BenchmarkUnmarshalParalell-8        4841752           232 ns/op         576 B/op           8 allocs/op
PASS
ok      github.com/bigwhite/protobufbench_goprotov2    6.355s

看来的确是这个问题。

从Benchmark结果来看,即便是与go protobuf v1相比,go protobuf v2生成的代码性能也要逊色一些,更不要说与gogo protobuf相比了。

三. 小结

从性能角度考虑,如果要使用go protobuf api,首选gogo protobuf。

如果从功能角度考虑,显然go protobuf v2在成熟稳定了以后,会成为Go语言功能上最为强大的protobuf API。

本文涉及源码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/protobuf

往期推荐

图解中文字符编码-Go语言例解

也谈Go的可移植性

写Go代码时遇到的那些问题[第3期]

Go 1.13中的错误处理 Go 1.14中值得关注的几个变化

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://51smspush.com/ smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展;短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx 微信公众号:iamtonybai 博客:tonybai.com github: https://github.com/bigwhite

微信赞赏:

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

go protobuf v1败给了gogo protobuf,那v2呢?相关推荐

  1. protobuf message定义_巧用 Protobuf 反射来优化代码,拒做 PB Boy

    作者:iversonluo,腾讯 WXG 应用开发工程师 有些后台同学将自己称为 SQL Boy,因为负责的业务主要是对数据库进行增删改查.经常和 Proto 打交道的同学,是不是也会叫自己 PB B ...

  2. 学习 protobuf(一)—— ubuntu 下 protobuf 2.6.1 的安装

    下载地址:https://github.com/google/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.gz(如果初次下载失败,不妨多试 ...

  3. php protobuf 二进制,PHP环境中使用ProtoBuf数据格式

    1.syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2 2.package test:定义包名为test,生成类时,会产生一个目录为test ...

  4. protobuf android 编译,Android跨平台编译 —— protobuf

    前言 正文 直入正题.编译protobuf的android版本我们使用的环境如下 CMake: 3.6 NDK: r16b Protobuf: v3.5.1 OS : Mac os 1  首先进入到c ...

  5. 使用 VXAPI ProtoBuf 工具抓取iPad微信Protobuf

    注:当前仅支持M1芯片的MAC电脑,如果您不是MAC电脑或MAC电脑不是M1芯片的,请勿下载,可以略过此文章. 准备工作 1. 已越狱的iPhone手机或已越狱的iPad,安装Cydia管理器  2. ...

  6. v1 中兴f450g_上海电信中兴F450G v2.0 改桥接

    并不需要拆机 张大妈有文章说要接TTL,实测不用,也许是地区差异或者估计纯粹为了拆机而拆机吧(因为拆机算硬件晒单有分数拿),其实还是常规套路,拔掉光纤,捅菊花重置机器(重置机器不会重置掉LOID,但是 ...

  7. Go protobuf

    使用protobuf实现节点间通信.编码报文以提高传输效率 protobuf全程Protocol Buffers,是Google开发的一种数据描述语言. protobuf是一种轻便高效的结构化数据存储 ...

  8. gogoclient java_链路跟踪-GRPC请求 - GoFrame官网 - 类似PHP-Laravel, Java-SpringBoot的Go企业级开发框架...

    在本章节中,我们将之前介绍HTTP Client&Server的示例修改为GRPC微服务,并演示如何使用GoFrame框架开发一个简单的GRPC服务端和客户端,并且为GRPC微服务增加链路跟踪 ...

  9. 【GoCN酷Go推荐】ip2location 解析 IP 地址库

     简介 很多时候,我们获取了用户ip,但是想知道更多信息,怎么办?使用ip2location吧. 这个库,可以从IP地址快速查找国家,地区,城市,纬度,经度,邮政编码,时区,ISP,域名,连接类型,I ...

最新文章

  1. gdal java shp_【GDAL/OGR】利用GDAL/OGR读取shp文件并转换为json文件(Java版)
  2. 活在无尽梦境的后续 β
  3. 连环卡通漫画《转学第一天》
  4. hdfs如何查找指定目录是否文件_hadoop实战教程-HDFS文件系统如何查看文件对应的block...
  5. String与StringBuffer和StringBuilder的根本区别
  6. CSS样式布局入门介绍,非常详尽
  7. 翻译: TensorFlow 2.0 中的新功能
  8. 进程的同步、互斥、通信的区别,进程与线程同步的区别
  9. Nopcommerce kendo UI Roxy Fileman控件的汉化
  10. 雷达一维距离像怎么用matlab仿真出来,雷达目标识别之一维距离像的学习
  11. Emlog大表哥资源网模板
  12. 搜狗输入法,输英语单词自动提示
  13. 【李宏毅】机器学习-RNN
  14. 大一计算机基础excel文档,大一计算机应用基础办公自动化软件深入Excel复习用PPT课件.ppt...
  15. 动态链接库和静态链接库
  16. 修改WSL的Ubuntu环境下ls显示的文件夹文字颜色和背景色
  17. mysql教程源码_MySql轻松入门系列————第一站 从源码角度轻松认识mysql整体框架图...
  18. IPSEC VXN配置实例
  19. python使用turtle绘制叠加等边三角形
  20. 程序员健身总动员:写代码后你胖了几斤?

热门文章

  1. 电脑误删文件恢复怎么做?数据恢复,4招就行!
  2. 100个Linux Shell脚本经典案例
  3. android 字体选中加粗,tablayout 选中文字加粗
  4. QQ扫描超级黑名单?无理取闹
  5. Micron将推出OLC NAND,是否沦为只能读取的SSD?
  6. 黑盒测试技术(边界值测试、等价类测试)
  7. vscode调试php
  8. decode 大于比较 小于_阐述Oracledecode函数的用法
  9. js apply()用法详解
  10. FCN(全卷积神经网络)详解