当前,gollvm将当前的g存储在tls中。runtime.getg()函数返回当前的g。这个函数将在某些情况下被内联以获得更好的性能,而GoSafeGetg传递将在某些情况下禁用内联。Cherry在这篇文章中描述了这种情况:
within a function,
//
// load g
// call mcall(...)
// load g
//
// may be compiled to
//
// leaq g@TLS, %rdi
// call __tls_get_addr
// movq %rax, %rbx // cache in a callee-save register %rbx
// ... use g in %rax ...
// call foo
// ... use g in %rbx ...
// This is incorrect if a thread switch happens at the call of foo.
一个实际的例子:gofrontend/chan.go#154
通过移除一个代码块中后续getg函数的内联,GoSafeGetg传递在linux/amd64上修复了这个问题。但是在Linux / arm64上,llvm在整个函数范围内对tls基本地址执行缓存优化,因此上述情况下获取到的第二个和后续的g可能仍然是错误的。
据我所知,这种优化在llvm和gcc中很常见,看起来是正确的,对于c / c来说非常好。在c / c引入类似goroutine的概念之前,我认为这种优化不会改变。请参考类似的问题:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=26461,https://bugs.llvm.org/show_bug.cgi?id=19177。
目前gollvm仅支持linux / amd64和linux / arm64平台,但随着未来更多平台的支持,我认为这个问题会在更多平台上出现,所以我认为我们需要找到一个更好的方法来存储当前的g。
目前我能想到的方法如下:
1.保持当前的做法,将g存储在tls中,尝试去除tls基本地址的缓存。
2.遵循main go的做法,预留一个寄存器来存储g。
3.将g存储在栈上的合适位置。
@thanm@cherrymui@ianlancetaylor 有任何建议吗?
4条答案
按热度按时间cvxl0en21#
个人来说,我真的很希望有一种方法能让后端不在GCC和LLVM中缓存TLS地址。我不认为这仅限于Go语言。在C语言中,如果你使用
getcontext
/setcontext
与TLS变量一起使用,这个问题也可能发生。但是我想getcontext
/setcontext
并不常用。正如你所说,这种情况可能不会很快发生。遵循Go主函数的惯例,预留一个寄存器来存储g。
我已经考虑过了。棘手的地方是libgo运行时使用C语言进行很多操作,包括外部C代码、libbacktrace、libffi、libgcc以及来自libc的系统调用 Package 器。如果我们全局预留一个寄存器,所有这些C代码都需要以特殊的方式编译。或者我们需要一些 Package 器来在C库边界处保存/恢复预留的寄存器。
将g存储在栈上的合适位置。
我不确定这将如何工作。此外,C代码似乎需要特殊的编译。
也许一个可能性是采用当前的方法,并添加一个机器IR pass,在CSE之后运行,该pass将getg调用内联回栈上。这将取决于机器。我也不确定是否可以为gccgo实现这一点。
gcmastyq2#
你好,Cherry,我也考虑了许多可能的解决方案,包括:
这里有一个在Linux/arm64上的例子:
使用"clang -O2 -o getg getg.c"编译上述getg.c文件。
如你所见,getg_new被内联了,这也避免了CSE优化的问题。那么我们就不再需要GoSafeGetg pass了。我不熟悉x86汇编,所以我没有写一个x86的例子,但我认为这种方法适用于其他架构。
当然,这种方法并不是完美的。如果在多个getg调用之间没有线程切换,这将导致一些不必要的指令被执行。但我认为这种性能损失非常小,而且很少有函数会频繁地调用getg。
你怎么看待这种方法?如果你觉得可以的话,我愿意发送一个补丁来解决这个问题。
juzqafwq3#
内联汇编可能不适用于动态链接。
7fyelxc54#
https://golang.org/cl/228737提到了这个问题:
gollvm: implement getg function with inline assembly for arm64