豆浆大叔

好惨,搞个 cgo 提案被教育了

前一阵又搞了个 cgo 提案,但是,被轮番教训了...

也不知道哪里勾起来他们的兴趣,一度让我怀疑是遇到了杠精,哈哈,可能对面的哥们也是一脸无语 ...

1 背景

Golang 默认对于从 Go 传入到 C 的内存会进行检查,如果内存中有 Golang 指针(指向 Golang GC 管理的内存),就会报 panic。

但是,我们在 Envoy Go 扩展里,为了性能和使用的便捷性,并不想要有这个检查,所以我们是依赖 GODEBUG=cgocheck=0 这个环境来关闭检查。 虽然 Go1.21 引入了 runtime.Pinner ,可以通过显式 Pin 的方式,将这些指针也传入 C。

2 提案

但是,上一次搞 cgo 内存优化 的时候,又想到了一个更好关闭这个检查的方式: 通过 #cgo nocheckpointer functionName 这种编译指令,来函数级别的关闭检查。

这个对我们来说,是更好的选择,上一次也写过好处:

  1. 可以在 C 函数级别指定生效,影响域足够小
  2. 可以写在 Envoy Go 的源码里,完全不需要用户关心

所以,有了这个提案:https://github.com/golang/go/issues/62425

3 分歧

很快,还没等到官方回应,就有人出来教育我了。

提炼一下,有效的分歧是:只有 cgocheck=0 才能运行的代码,是否是安全的

经过的一系列的正反方的拉锯:

反方:依赖 Go 内置类型的内存布局是未定义的行为,不靠谱

正方:cgo 会生成 _GoString_ 这些 struct 给 C 来使用,依赖是合理的 反方:没有 Pin 的内存,在 C 使用的时候,可能已经被 GC 释放掉了

正方:引用关系还在的,我们在当前这次 C 函数使用是安全的

反方:那 GC 还有可能移动对象呢

正方:至少目前的 GC 是不会移动的,Go 这种指针暴露给用户了的,大概率以后的 GC 也不会搞成移动的;就算以后搞了,大概率也不好保证完全向后兼容,既然到时候会破坏向后兼容,那破坏下这一个编译指令,也不是什么大事了

反方:累了,懒得跟你说了 ...

4 收场

刚开始的时候,ian  大佬还把这个提案,放到了 Incoming proposal 里。

但是,经过一个周末的来回 pk,ian 大佬还是出手把这个给关了。

给出的理由是: making it easier to break the rules ,也就是希望更多保持现有规则。

至于规则的原因,甩了一个 2015 年他设计这个规则时的讨论贴:https://github.com/golang/go/issues/12416

5 原因

过了一遍 issue,总结这么几个点:

  1. 目前的规则,就是考虑了未来,为 GC 实现移动对象留下空间
  2. 虽然现在可以直接将指针传给 C,未来实现移动 GC 的时候,cgo 编译器会为这些指针生成 Pin 代码,让 GC 不移动这些指针
  3. 但是,不允许指针指向的内存中再含有指针,是一个折中考量。这种情况出现的不多,如果以后也自动 Pin 的话,可能导致实现比较复杂

好吧,这回算是搞清楚了。

这个 cgocheck 检查就是为了未来的移动 GC 而预留的,所以,关闭 cgocheck 检查,至少现在还是安全的。

虽然从我个人的角度看,以后改成移动 GC 的可能性不大,但是官方大佬并不希望给自己埋雷。

6 填坑 runtime.Pinner

好吧,既然不让搞,那就还是老老实实用 runtime.Pinner 吧。 但是,我的提案中,也提到了 runtime.Pinner 的坑: Pin 指针的时候,指针必须指向 Go GC 中的地址,如果不是的话,会直接 panic。 这个让使用 Pin 变得很难的,比如,常量 string 的 data 指针,就直接指向 rodata 段中的内存,这个作为普通用户是很难判断的。

刚好看到也有个 issue 在抱怨,rsc 大佬说,这种情况可以直接忽略,于是又搞了这个补丁:https://go-review.googlesource.com/c/go/+/527156

好在这个改动没啥分歧,比较快就被合并了。

7 Envoy Go

至此,应该可以基于 runtime.Pinner 来实现 cgocheck clean 的 Envoy Go extension 了。 至少默认情况下,可以不依赖于用户手动设置 GODEBUG 环境变量了,毕竟,很多时候一不小心就忘了,而且很多时候,或许大家也不是那么的关心这一丢性能。 对于一些将 string 指针传给 C 的,可能就老老实实的改成 data 指针 + 长度,分开两个参数来搞。对于复杂的传参,那就用 runtime.Pinner 吧。 至于,是否提供一个可选的编译指令,来直接跳过 runtime.Pinner 的开销,这个还得后面再压测一把看看的了。

8 最后

哈哈,虽然这个提案被喷惨了,不过好歹咱也是玩过微博,混过社区的人,这点破事也算不上啥。

虽然没人喜欢被怼,但是,折腾一波,也搞清楚了 cgocheck 的前因后果,也不白折腾。

咱也就不纠结了,毕竟这种折中取舍的事情,主要看投票权的,估计也就是 ian 大佬说了算了。