0%

cgo 优化:被踢出来三次,终于进入 Go 1.21 rc1 版本

没错,这就是之前介绍过的 C 调用 Go 快十倍的优化,也是 很快被打脸 的后续。

算上第一次被打脸,这个补丁一共被踢出来三次,好在第四次合入之后,目前已经进入了 Go 1.21 rc1 版本,后续再被踢出来的概率应该比较低了。

第一次被踢

回顾下第一次被踢,简单说,是因为 g0 栈的 stack.lo 只是在当前栈顶往下多留了 32k,只适用于当前这一次 cgo 调用,后续的 cgo 调用,栈顶位置可能已经超出 stack.lo 了,就直接栈溢出了。

Cherry 大佬的修复方案是,将 stack.lo 设置为当前 C 栈的栈顶,也就是 g0 的栈顶,直接跟 C 栈的栈顶对齐了,也就是 g0 的 stack.lo 直接拉到最低。

具体细节,这里就不做过多介绍了,可以看 之前打脸的文章

第二次被踢

第二次是因为 TSAN 检测到了 race,具体情况是:

  1. 从 C 的角度来看:不同的 C 线程在获取 M 的时候,有可能会竞争写同一个 M 的 M1.g0.stack.lo,导致 race。
  2. 但是呢,TSAN 只分析了 C 代码,并不知道在 Go 侧获取 M 是有锁的

所以,这是实际上是 TSAN 的一个误报,具体细节可以看 Michael Pratt 大佬的分析:
https://github.com/golang/go/issues/59678#issuecomment-1512114382

解决办法是,不再传 g0 的地址到 C 了,而是传一个 stack.lo 的地址给 x_cgo_getstackbound 这个 C 函数,这样 C 侧看到的就是两个地址了,就没法分析竞争了,也就不会误报了。

具体可以见这个 CL:
https://go-review.googlesource.com/c/go/+/485500

第三次被踢

第二次被踢还是有点无辜,第三次被踢则是真的有两个 bug:

Go 线程数限制

Go 默认是有最大 10k 线程数的限制,是通过 M 的计数来实现的。

不过原来 extra M 也是计算在内的,因为我们的 extra M 会被 C 线程绑定,这会导致一种情况,如果有 10 k 个 C 线程绑定了 extra M,那么 M 数量直接就到了 10 k 了。

这里的解决方案是,计算 Go 线程数量的时候,extra M 不计算在内,这样就只限制 Go 线程的数量,C 线程的数量,Go 本来也控制不了的。

具体可以见 Michael Pratt 在这里的描述:
https://github.com/golang/go/issues/60004

stack.hi

第一次踢出来是因为 stack.lo 栈顶,那是在 Go 在动态检测是否需要扩栈的时候用到的。

这次是则是因为 stack.hi 栈底,这个则是在一个比较冷门的地方用到的,在 Go 收到系统信号的时候,就有一处判断 adjustSignalStack 是,当前栈帧 sp,是否在 Go stack 中,也就是 sp >= stack.lo && sp < stack.hi,来判断是否为来自非 Go 代码的触发。

具体可以见 Michael Pratt 在这里的描述:
https://github.com/golang/go/issues/60007

关于 cgo 场景下 Go 对于信号的处理,确实是比较绕的,之前也写过一篇文章介绍过:
https://uncledou.site/2022/go-cgo-c-to-go/

解决方案跟 stack.lo 也是类似的,也就是在 C 侧直接将 stack.hi 拉到最高,跟 C 栈的栈底对齐。

具体修复在这里:
https://go-review.googlesource.com/c/go/+/495855/

最后

相对来说,后面两次被踢信息量没那么大,还是 cgo 场景下,g0 这个特殊 goroutine 的栈空间问题。

最后,希望不要再被踢出来了… 确实也是够折腾的…

如果你也有 cgo 的场景,欢迎联系,一起交流学习