0%

cgo 优化后续,打脸来得真快

cgo 优化被合并主干之后,很高兴的写了篇流水账,倾诉了一番心酸史,也感谢各位大佬的转发,收获了写公众号以来最多的围观

然而打脸来得也真快,就在昨儿,愚人节的早上,被 revert 了 …

好在 Cherry 大佬还有意继续推进,搞了个 CL 481061 ,Michael 大佬也给了 +2 approve,希望能合回去,赶上 1.21 这个版本。

挖坑小能手

我们看看 Cherry 大佬最新这个 CL 的 commit log 中的描述:

1
2
3
4
5
6
7
8
CL 392854, by doujiang24 <doujiang24@gmail.com>, speed up C to Go
calls by binding the M to the C thread. See below for its
description.
CL 479255 is a followup fix for a small bug in ARM assembly code.
CL 479915 is another followup fix to address C to Go calls after
the C code uses some stack, but that CL is also buggy.
CL 481057, by Michael Knyszek, is a followup fix for a memory leak
bug of CL 479915.

最初我搞的 CL 392854 被合并之后,大佬们已经帮忙搞了三个 bugfix 了,真是惭愧…

最开始的 CL 479255,是一个低级 bug,arm 的汇编少复制了一行,没啥好说的

morestack on g0

CL 479915 则是费了一番功夫才搞明白…

Cherry 大佬的描述在这个 issue 59294

g0 是干啥的

要讲清楚这个 bug,得先介绍一下 g0

每个 M 都有一个 g0,来处理一些特殊的事情,比如扩栈的时候,newstack 函数就运行在 g0 上。

如下 morestack 的代码:

1
2
3
4
5
6
7
// Call newstack on m->g0's stack.
MOVQ m_g0(BX), BX
MOVQ BX, g(CX)
MOVQ (g_sched+gobuf_sp)(BX), SP
MOVQ (g_sched+gobuf_bp)(BX), BP
CALL runtime·newstack(SB)
CALL runtime·abort(SB) // crash if newstack returns

这里是先切换到 g0 栈,再运行的 newstack,结合 g 的数据结构,核心就是这行伪代码

1
SP = g0.sched.sp

问题

但是,在 cgo 启用的时候,g0 并不会像普通的 g 一样,拥有自己的 stack 空间,而是会复用 C 线程的 stack 空间。

我们可以看 needmg0 栈的处理,g0 栈就是当前 C 栈顶往下的 32 kb 地址空间

1
2
3
gp.stack.hi = getcallersp() + 1024 // 这个 1024 没搞懂是为啥
gp.stack.lo = getcallersp() - 32*1024
gp.stackguard0 = gp.stack.lo + _StackGuard

优化之前,每次 c 调用 go 都会执行 needm,也就是 g0 的栈会根据当前 c 栈来动态计算;

但是,优化之后,并不是每次 c 调用 go 都会执行 needm 了,也就是 g0 栈固定在第一次进入 go 时计算的栈空间了。

也就是 stackguard0 是固定的了,如果后续 c 调用 go 的时候,c 栈比第一次高很多,这可能就会导致,runtime 在栈检查的时候认为 g0 栈 overflow 了;而 g0 的栈是不能扩的,也就会抛 morestack on g0 的异常。

栈检查

这里稍微解释下 Go 栈检查的逻辑,比如 amd64 上,我们经常会看到这样的指令:

1
2
3
4
5
6
000000000008b100 <runtime.main.func1>:
8b100: 49 3b 66 10 cmp rsp,QWORD PTR [r14+0x10]
8b104: 76 2d jbe 8b133 <runtime.main.func1+0x33>
...
8b133: e8 e8 8a 02 00 call b3c20 <runtime.morestack_noctxt.abi0>
8b138: eb c6 jmp 8b100 <runtime.main.func1>

核心是这样的伪代码:

1
if rsp <= g.stack.stackguard0; { runtime.morestack_noctx() }

也就是拿栈顶 rspstackguard0 对比,小于 stackguard0,则需要扩栈。

对应上面问题的场景,也就是 stackguard0 是固定的,但是 rsp 则是每次不一样的。

解决办法

Cherry 大佬在 issue 里说的解法有两种:

  1. 每次 c 调用的时候,重新为 g0 计算栈空间,这样就跟优化之前保持一致的行为。
  2. g0 的栈顶地址,设置为 c 的栈顶;本质上就是把 g0 的栈空间搞到最大,这样就不容易达到扩栈的条件

大佬在 CL 479915 选择了第二种解法,patch 都搞到 13 个版本才合并,我一路看下来,最大的感觉是,系统兼容性真难搞。

不过,这里还有一个小隐患,也就是栈顶是直接拉满够用了,但是栈底实际上还是不对的,比如 morestack 切栈的时候,会不会切到 c 栈使用了的空间,把 c 栈写坏了呢?

目前看下来,即使在复用 M 的时候,也会有这段逻辑,也就是会将真实的 rsp 存入 g0.sched.sp,所以看起来貌似还好。不过这个还是比较的 tricky,会不会有其他的坑,其实也说不太好。

1
2
3
4
MOVQ	m_g0(BX), SI
MOVQ (g_sched+gobuf_sp)(SI), AX
MOVQ AX, 0(SP)
MOVQ SP, (g_sched+gobuf_sp)(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!