cgo 优化被合并主干之后,很高兴的写了篇流水账,倾诉了一番心酸史,也感谢各位大佬的转发,收获了写公众号以来最多的围观
然而打脸来得也真快,就在昨儿,愚人节的早上,被 revert 了 …
好在 Cherry 大佬还有意继续推进,搞了个 CL 481061 ,Michael 大佬也给了 +2 approve,希望能合回去,赶上 1.21 这个版本。
挖坑小能手
我们看看 Cherry 大佬最新这个 CL 的 commit log 中的描述:
1 | CL 392854, by doujiang24 <doujiang24@gmail.com>, speed up C to Go |
最初我搞的 CL 392854 被合并之后,大佬们已经帮忙搞了三个 bugfix 了,真是惭愧…
最开始的 CL 479255,是一个低级 bug,arm 的汇编少复制了一行,没啥好说的
morestack on g0
CL 479915 则是费了一番功夫才搞明白…
Cherry 大佬的描述在这个 issue 59294
g0 是干啥的
要讲清楚这个 bug,得先介绍一下 g0
每个 M
都有一个 g0
,来处理一些特殊的事情,比如扩栈的时候,newstack
函数就运行在 g0
上。
如下 morestack
的代码:
1 | // Call newstack on m->g0's stack. |
这里是先切换到 g0
栈,再运行的 newstack
,结合 g
的数据结构,核心就是这行伪代码
1 | SP = g0.sched.sp |
问题
但是,在 cgo 启用的时候,g0
并不会像普通的 g
一样,拥有自己的 stack 空间,而是会复用 C 线程的 stack 空间。
我们可以看 needm
对 g0
栈的处理,g0
栈就是当前 C 栈顶往下的 32 kb 地址空间
1 | gp.stack.hi = getcallersp() + 1024 // 这个 1024 没搞懂是为啥 |
优化之前,每次 c 调用 go 都会执行 needm
,也就是 g0
的栈会根据当前 c 栈来动态计算;
但是,优化之后,并不是每次 c 调用 go 都会执行 needm
了,也就是 g0
栈固定在第一次进入 go 时计算的栈空间了。
也就是 stackguard0
是固定的了,如果后续 c 调用 go 的时候,c 栈比第一次高很多,这可能就会导致,runtime 在栈检查的时候认为 g0
栈 overflow 了;而 g0
的栈是不能扩的,也就会抛 morestack on g0
的异常。
栈检查
这里稍微解释下 Go 栈检查的逻辑,比如 amd64 上,我们经常会看到这样的指令:
1 | 000000000008b100 <runtime.main.func1>: |
核心是这样的伪代码:
1 | if rsp <= g.stack.stackguard0; { runtime.morestack_noctx() } |
也就是拿栈顶 rsp
与 stackguard0
对比,小于 stackguard0
,则需要扩栈。
对应上面问题的场景,也就是 stackguard0
是固定的,但是 rsp
则是每次不一样的。
解决办法
Cherry 大佬在 issue 里说的解法有两种:
- 每次 c 调用的时候,重新为
g0
计算栈空间,这样就跟优化之前保持一致的行为。 - 将
g0
的栈顶地址,设置为 c 的栈顶;本质上就是把g0
的栈空间搞到最大,这样就不容易达到扩栈的条件
大佬在 CL 479915 选择了第二种解法,patch 都搞到 13 个版本才合并,我一路看下来,最大的感觉是,系统兼容性真难搞。
不过,这里还有一个小隐患,也就是栈顶是直接拉满够用了,但是栈底实际上还是不对的,比如 morestack
切栈的时候,会不会切到 c 栈使用了的空间,把 c 栈写坏了呢?
目前看下来,即使在复用 M 的时候,也会有这段逻辑,也就是会将真实的 rsp
存入 g0.sched.sp
,所以看起来貌似还好。不过这个还是比较的 tricky,会不会有其他的坑,其实也说不太好。
1 | MOVQ m_g0(BX), SI |
又来 revert
而然在 CL 479915 合并之后第二天,Cherry 大佬就来了个 revert CL ,直接把 cgo 优化全 revert 了。原因是新的修复,发现了更多了 breakage。
不过是啥错误,也没有细说,不过,Michael 大佬提了另外一个 CL 481057 ,看起来是内存泄漏导致了 sanitizers 异常。
好在呢,Cherry 大佬又提了个 CL 481061 ,打算重新把 cgo 优化合进来,虽然 Michael 大佬已经给了 +2 approve,但是还没合。
比较让人担心的是,不清楚 google 内部的测试,是否还有其他异常,希望顺利吧 …
最后
其实,之前对于 g0
的理解是不到位的,以为是分配了单独的栈空间,实际上开启 cgo 时 g0
是复用的 c 线程栈。
果然,没理解到位的,终将是要付出代价的。辛苦了大佬们帮忙填坑,哈哈
这是第二次 CL 被 revert 了,真心不好玩,不过,确实是自己没搞好,也挺感谢有这些测试,也很感叹工程化能力真牛,respect!