``` gollvm: 找到更好的处理g的方法 ```

dzjeubhm  于 6个月前  发布在  Go
关注(0)|答案(4)|浏览(46)

当前,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 有任何建议吗?

cvxl0en2

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实现这一点。

gcmastyq

gcmastyq2#

你好,Cherry,我也考虑了许多可能的解决方案,包括:

  1. callee saved register。这实际上是目前的主要Go方法。我们需要处理许多特殊情况(tsan、msan、vdso等)。正如你在gollvm中所说的,我们可能还需要为许多C代码进行特殊的编译。这种方法过于繁琐。
  2. free system register。目前没有找到这样的寄存器。
  3. 在栈上的某个地方。这种方法需要对goroutine的栈有一些非常丑陋的限制。例如,将g放在栈底,并要求栈与2m字节对齐,栈的大小不能超过2m。然后我们可以从sp的值计算出g的位置。我认为这只是一种选择,我们知道这种方法非常丑陋。
  4. TLS。这种方法需要处理CSE优化对TLS基址的缓存。目前,这种方法是最可行的方法。在linux/amd64上,我们编写了一个禁用CSE优化的pass。但也许我们可以用更简单的方式禁用这个优化,那就是内联汇编。
    这里有一个在Linux/arm64上的例子:
#include <stdio.h>
#include <stdlib.h>

__thread long long g = 1;
void schedule_this_routine_to_a_different_thread() {
  printf("schedule_this_routine_to_a_different_thread\n");
}

// The following getg simulates the getg in gollvm
long long getg() {
  return g;
}

// getg_new is implemented with inline assembly
long long getg_new() {
  long long out;
   asm volatile(
     "mrs %[out], tpidr_el0 \t\n\
      add %[out], %[out], #:tprel_hi12:g, lsl #12 \t\n\
      add %[out], %[out], #:tprel_lo12_nc:g \t\n\
      ldr %[out], [%[out]] \t\n\
     " : [out] "=r" (out)
   );
  return out;
}

long long test_getg() {
  long long v1, v2;
  v1 = getg();
  schedule_this_routine_to_a_different_thread();
  v2 = getg();
  return v1 + v2;
}

long long test_getg_new() {
  long long v1, v2;
  v1 = getg_new();
  schedule_this_routine_to_a_different_thread();
  v2 = getg_new();
  return v1 + v2;
}

int main() {
  int res = 0;
  res += test_getg();
  res += test_getg_new();
  return res;
}

使用"clang -O2 -o getg getg.c"编译上述getg.c文件。

The objdump result:

    00000000004005f0 <test_getg>:
  4005f0:       a9be4ff4        stp     x20, x19, [sp, #-32]!
  4005f4:       a9017bfd        stp     x29, x30, [sp, #16]
  4005f8:       d53bd048        mrs     x8, tpidr_el0
  4005fc:       91400108        add     x8, x8, #0x0, lsl #12
  400600:       91004113        add     x19, x8, #0x10
  400604:       f9400274        ldr     x20, [x19]
  400608:       90000000        adrp    x0, 400000 <g+0x400000>
  40060c:       911e8000        add     x0, x0, #0x7a0
  400610:       910043fd        add     x29, sp, #0x10
  400614:       97ffffa3        bl      4004a0 <puts@plt>
  400618:       f9400268        ldr     x8, [x19]
  40061c:       a9417bfd        ldp     x29, x30, [sp, #16]
  400620:       8b140100        add     x0, x8, x20
  400624:       a8c24ff4        ldp     x20, x19, [sp], #32
  400628:       d65f03c0        ret

  000000000040062c <test_getg_new>:
  40062c:       f81e0ff3        str     x19, [sp, #-32]!
  400630:       90000000        adrp    x0, 400000 <g+0x400000>
  400634:       911e8000        add     x0, x0, #0x7a0
  400638:       a9017bfd        stp     x29, x30, [sp, #16]
  40063c:       910043fd        add     x29, sp, #0x10
  400640:       d53bd053        mrs     x19, tpidr_el0
  400644:       91400273        add     x19, x19, #0x0, lsl #12
  400648:       91004273        add     x19, x19, #0x10
  40064c:       f9400273        ldr     x19, [x19]
  400650:       97ffff94        bl      4004a0 <puts@plt>
  400654:       d53bd048        mrs     x8, tpidr_el0
  400658:       91400108        add     x8, x8, #0x0, lsl #12
  40065c:       91004108        add     x8, x8, #0x10
  400660:       f9400108        ldr     x8, [x8]
  400664:       a9417bfd        ldp     x29, x30, [sp, #16]
  400668:       8b130100        add     x0, x8, x19
  40066c:       f84207f3        ldr     x19, [sp], #32
  400670:       d65f03c0        ret

如你所见,getg_new被内联了,这也避免了CSE优化的问题。那么我们就不再需要GoSafeGetg pass了。我不熟悉x86汇编,所以我没有写一个x86的例子,但我认为这种方法适用于其他架构。
当然,这种方法并不是完美的。如果在多个getg调用之间没有线程切换,这将导致一些不必要的指令被执行。但我认为这种性能损失非常小,而且很少有函数会频繁地调用getg。
你怎么看待这种方法?如果你觉得可以的话,我愿意发送一个补丁来解决这个问题。

juzqafwq

juzqafwq3#

内联汇编可能不适用于动态链接。

7fyelxc5

7fyelxc54#

https://golang.org/cl/228737提到了这个问题:gollvm: implement getg function with inline assembly for arm64

相关问题