cgo 内存优化合并之后,本来说,最近太卷了,等有空一点了,再把这个优化集成到 Envoy Go 里面去。
但是,看到这个优化,还比较的受欢迎,比如:golang-fips/openssl 已经用上了,tinygo 也增加了这个语法支持。
一想,作为始作俑者,咱也不能落后呀,于是又卷了一把自己,趁着周末搞了一把优化,具体见:
https://github.com/envoyproxy/envoy/pull/29396
优化实现
因为这个 cgo 这个优化,只是加了一些指令,帮助 cgo 编译器不再强制将参数 escape 到堆上,所以,我们只需要给一些 C 函数加上新的指令即可。
比如这些有将 Go 指针传入到 C 的:
1 | #cgo noescape envoyGoFilterHttpCopyHeaders |
不过,还是有两个小的点可以分享一下的。
版本兼容性
因为新增的指令在 1.21 之前是没有的,老版本编译器看到这些会报错,所以,我们需要针对 1.22 才生效。
所以,单独搞了个文件,并加了上编译指令:
1 | //go:build go1.22 |
slice 内存优化
在获取 Header 的 API 中,我们是预先在 Go 侧申请内存,也就是通过 make slice 的方式。
如果我们想让这个内存也留在栈上,除了加上新的指令,还有需要一个改动。
原因是 make slice
的时候,如果长度是一个变量,那么 Go 编译器就会将这个 slice 给 escape 到堆上,因为函数栈空间大小是编译期间就计算好的,没法动态算的。
所以,我们这里取舍一下:
1 | if num <= maxStackAllocedHeaderSize { |
对于大部分 header 数量少于 maxStackAllocedHeaderSize
的,则直接使用栈空间,如果超过了,则还是动态申请,用堆上内存。
这个 maxStackAllocedHeaderSize
太大了也不好,因为是函数栈空间大小是预先计算的,每次执行函数都会预先准备的,虽然相对开销低,但是也不是完全零成本,太大了浪费成本也不能忽视。
所以,现在目前是拍脑袋定的 16
,没有仔细的调研/压测过,以后有空再对比下的,哈哈
不过,这种值也只能是尽量适用于大多数场景了,没法完全通用的了。好在这只是很小的一个优化,在多数情况下,其实对整体影响并不大。
优化效果
说实话,我们对这个效果是有预期的,铁定高不了多少。
之前搞的 cgo CPU 优化,那是每次 C 调用 Go 能提升 10+ 倍,减少 1000+ ns,在压测的时候能让简单场景下的 qps 提升约 10%。
而这次的内存优化,在上一篇介绍过单个 string 指针参数的 benchmark,只能提升约 20%,减少 20+ ns,这个量级差距还是很明显的。
这次的压测场景是,在 DecodeHeader
和 EncodeHeader
都有类似的逻辑:获取 Header
和设置 header
,具体可见这里的代码:
https://github.com/doujiang24/envoy-filter-benchmark/tree/main/golang-header-get-set
还是上一次压测的环境,阿里云 2c4g,加 wrk 压测。
QPS 提升约 0.5-1.0%,GC 次数减少约 50%
怎么说呢,虽然确实不太明显,但是很符合预期
网关这种场景里,有很大部分的基础开销是 proxy 的基础逻辑,比如接受网络请求,发送网络请求。要搞一些底层的优化,对整体有较大提升的,其实还是比较难的了。