第二个 cgo 优化的补丁 合并 Go 官方仓库了,哈哈,照例写个文章吹吹水 :)
嗯,之前一波三折的 CPU 耗时优化 算第一弹,这个就算第二弹了~
背景
这个内存优化的背景,之前也写过一篇,记一个 Go 编译器优化提案,详细描述了为什么要搞这个优化。
简单说就是,希望 Go 对象传给 C 的时候,不要强制把 Go 对象逃逸到堆上。因为一个对象能保留在栈上,性能开销是更低的。
具体的做法呢,提供了两个 annotation 标记:
#cgo noescape functionName
表示传给
functionName
这个 C 函数的 Go 对象,不需要强制逃逸到堆上#cgo nocallback functionName
申明
functionName
这个 C 函数,不会有回调 Go 函数,此时 runtime 会加入一个检查,如果实际检测到这个 C 函数回调了 Go 函数,那就直接 panic
想了解详情的朋友,可以回看之前的文章。今天这篇呢,主要记录下实现机制。
实现机制
首先,交代一个背景知识,cgo是 golang 自带的一个小编译工具,比如,我们写的 Go 调用 C 的代码,会先被 cgo预编译一次,然后才用标准的 Go 编译器编译。
感兴趣的朋友,可以移步这篇 从编译器视角看 cgo
了解这个背景之后,相对来而言,这个补丁逻辑是比较简单的了:
- 提取 annotation 信息
- 跳过生成
_Cgo_use
代码,这样 noescape 的效果就实现了 - 从 Go 进入 C 的时候,在 goroutine 上打个标,从 C 调用 Go 函数的时候,检查这个标,这样就可以实现 nocallback的运行时检查
简单来说,这个补丁的改动,基本都在 cgo 这个子编译工具里。
提取 annotation
cgo 这个编译工具,逻辑上来说,也是比较简单的,因为 Go 语法解析生成 AST,还是复用的标准 Go 的那一套。
并且,对于 Go 源码中,在注释中申明的 C 代码,也并没有一个语法解析器,只是简单的按照文本匹配的方式在处理。
至于 C 代码的合法性,以及 C 代码中的函数等信息,Go 直接调用 C 编译器来编译,然后解析 C 编译器产生 dwarf 信息来提取,也不失为一种好的选择。
所以,提取 annotation,我们也只是按照文本匹配的方式处理,找到对应的地方,实现就很简单了。
跳过 _Cgo_use
这个是在 cgo 编译器的输出阶段,根据提取的 annotation 信息,选择性的跳过即可。
不过,这地方有个小插曲,当时跳过了 _Cgo_use
之后,发现还是会强制逃逸到堆上。
经过一番分析,发现还有一个地方,也会触发强制逃逸:func _cgoCheckPointer(interface{}, interface{})
这个跟 _Cgo_use(interface{})
是一个效果,指针类型的对象转为 interface{}
类型的时候,就会逃逸。
经过 Ian Lance 大佬指点,为 _cgoCheckPointer
加上了 //go:noescape
,这事才算了了。
关于 //go:noescape
其实也值得说道说道,不过今天就不聊了,以后有空再说吧。
运行时 nocallback 检查
如果对 C 调用 Go 和 Go 调用 C,这两个 cgo 的调用流程比较清楚的话,这个实现也就比较简单了。
主要是实现了一个 cgoNoCallback(v bool)
的 runtime 函数,通过它来设置 goroutine 中的nocgocallback
的标
在 Go 调用 C 的时候,调用
_Cgo_no_callback(true)
从 C 返回 Go 的时候,
_Cgo_no_callback(false)
检查则是在 cgocallbackg
中完成,这是 C 调用 Go 中的一个入口函数。
如果对 cgo 调用流程不够了解,且有兴趣的话,可以移步这两篇,C 调用 Go,Go 调用 C。
效果
搞了一个 benchmark,对于将一个 Go string 传入 C 的场景,87 ns
vs 61 ns
提升了 20+ ns
,这个效果比我预想的略好一些,哈哈。
1 | BenchmarkCgoCall/string-pointer-escape |
至于在 MoE 框架中的真实效果,后面得空在 Envoy Go 中集成了这个优化,测试一把之后,再来吹水了。
值得一提的是,这个优化的是 Go 调用 C 的场景,也就是当 Go 代码中有比较多的调用,比如 getHeader
,setHeader
这种操作的时候,效果才出得来。如果是简单的 Passthrough 场景的压测,那应该是看不出来区别的。
也就是,复杂的场景,交互比较多的场景,效果会更明显。
彩蛋
在实现的过程中发现,对于 Envoy Go 目前依赖 GODEBUG=cgocheck=0
这个环境变量来关闭 cgocheckpointer
的坑,或许有了新的更好的解法:
#cgo nocheckpointer functionName
申明这个 C 函数的参数,不需要 cgocheckpointer
,好处是:
- 可以在 C 函数级别指定生效,影响域足够小
- 可以写在 Envoy Go 的源码里,完全不需要用户关心
- 实现也简单,跳过
_cgoCheckPointer
的生成就行
以后有空了,可以试试再搞个 proposal,最近太卷了,没得空玩了~
最后,如果你对 cgo 也感兴趣,欢迎一起交流~