0%

记一个 Go 编译器优化提案

去年 10 月搞的提案,最近终于被接受了,第一个被接受的提案,写篇文章记录下的,嘿嘿

故事

去年在搞 MoE,MOSN on Envoy 新架构,折腾了不少 cgo。在分析 cgo 实现的时候,发现了一个 GC 相关的优化点,于是就搞了个提案。

10 月份提交的,一直没动静。今年 2 月开始活跃了,经过简单几轮讨论,就被接受了,整体还算比较顺利的。

之所以会比较顺利,我估计也是搭上了 Go team 的便车,看起来是他们想优化一下 escape analysis,这个提案刚好可以 match,给 escape analysis 更多的信息。

场景

我们知道 C 函数只有一个返回值,Go 调用 C 的时候,如果需要多个返回值,那么 C 函数如何设计呢?

首先,我们希望 Go 和 C 之间的交互足够简单,每次调用都是独立的。比如,我们并不希望在 C 侧申请内存,然后给 Go 返回内存指针。因为这样的话,我们还需要显式释放内存。

那么,最好的办法,就是按照 C 的套路玩,将 Go 对象内存指针,通过参数传给 C,在 C 里给 Go 的内存对象赋值。比如,在 Envoy Go 扩展里,我们就是这么玩的,可以看这个简单的示例:

1
2
3
4
5
6
7
8
9
func (c *httpCApiImpl) HttpGetIntegerValue(r unsafe.Pointer, id int) (uint64, bool) {
var value uint64
res := C.envoyGoFilterHttpGetIntegerValue(r, C.int(id), unsafe.Pointer(&value))
if res == C.CAPIValueNotFound {
return 0, false
}
handleCApiStatus(res)
return value, true
}

这里,我们将一个 uint64 内存对象的地址传给 C,是不是看起来比较清爽了呢。

问题

单从 Go 代码来看,value 是可以放在 stack 上的,但是,由于有了 cgo 调用,目前的实现,会将 value escape 到 heap 上,这会加重 GC 负载。

尤其在 MoE 这种网关场景中,这种代码是跑在请求处理的热路径上的,并且 Go 代码中可能会比较频繁的进行这类调用,也就是有很多这类 GC 对象 escape 到 heap 中,因此造成的 GC 开销,应该也是不小的。

实现

为了确保总是会 escape,cgo compiler 会生成的这样的 Go 包裹代码:

1
2
3
4
5
6
7
func _Cgo_use(interface{})

func _Cfunc_xxx(xxx) {
if _Cgo_always_false {
_Cgo_use(p0)
}
}

目的是,欺骗 escape analysis,如果 p0 是指针,则总是会逃逸。

并且,由于 _Cgo_always_false 总是为假,在编译优化阶段,这个分支又被优化掉了。

原因

为什么有了 cgo 调用,就需要 escape 呢?

Go stack

这里需要先简单介绍一下 Go stack:

目前版本里,Go stack 大小是可变的,而且是连续内存,当生长/缩小的时候,会重新申请内存段,将老的内存从 old stack copy 到 new stack。

并且 copy 过程中会进行比较复杂的栈上指针映射转换,也就是说,stack 上也可以有指针变量指向 stack 上的地址。

但是,如果指针变量已经传给 C,那是没有办法做指针映射转换了,也就是栈发生移动的时候,C 拿到的地址就是非法的了。

时机

那什么情况下,会出现呢?

最开始想到的是,在进入 C 之后,如果 Go 发生了 GC,可能会触发缩栈,但是后来仔细看代码,进入 C 之后,缩栈是被禁掉了的。

另外一个是,不太常用的场景,C 如果又回调了 Go,那么在 Go 里面是可以伸缩栈了,如果在回到 C,此时 C 中的地址就是非法的了。

解决办法

增加 annotation,让编译器感知不需要 escape,不生成 _Cgo_use 就可以了。

一开始我提的是 go:cgo_unsafe_stack_pointer,Go team 觉得不好,最后是 rsc 大佬提的:#cgo noescape/nocallback

感兴趣的话,可以看 proposal 的讨论:
https://github.com/golang/go/issues/56378

最后

提案是接受了,实现还是有些工作量的,主要是这个 annotation 是放在 C code 里的,目前 Go 对于 C 代码并没有 parser。
剩下的部分,应该是比较简单的了。

后面有空在搞了。