【问题标题】:How fast is thread local variable access on LinuxLinux上的线程局部变量访问有多快
【发布时间】:2012-04-12 04:35:06
【问题描述】:

在 Linux 中访问线程局部变量的速度有多快。从 gcc 编译器生成的代码中,我可以看到它使用了fs 段寄存器。显然,对线程局部变量的访问不应该花费额外的周期。

但是,我一直在阅读有关线程局部变量访问缓慢的恐怖故事。怎么会?当然,有时不同的编译器使用与使用fs 段寄存器不同的方法,但是通过fs 段寄存器访问线程局部变量是否也很慢?

【问题讨论】:

  • 幕后发生了什么:akkadia.org/drepper/tls.pdf .. 有没有人有动力阅读这篇文章并在简短的回答中进行总结? :D
  • “恐怖故事”可能来自 TSS(线程特定存储)通过 pthreads_setspecific。 TSS 比 TLS 慢,但如果处理得当也不会慢很多。
  • 我可以给你一个关于 non 线程局部变量(一个简单的整数计数器)缓慢的恐怖故事,该变量通过多个线程进行了修改并将系统速度降低到由于缓存窥探而导致的爬网。使其成为线程本地并在最后对所有线程本地进行求和,这使我的加速倍数提高了 100 倍或类似。
  • drhirsch:该死的人!我正在使用的工具有完全相同的问题,我完全像你一样解决了它,也就是说,改用线程局部变量:)!干杯!

标签: c linux multithreading gcc x86-64


【解决方案1】:

但是,我一直在阅读有关线程局部变量访问缓慢的恐怖故事。怎么会?

让我用我从http://software.intel.com/en-us/blogs/2011/05/02/the-hidden-performance-cost-of-accessing-thread-local-variables 中获取的示例来演示Linux x86_64 上线程局部变量的缓慢性。

  1. 没有__thread 变量,没有缓慢

    我将以此测试的表现作为基础。

        #include "stdio.h"
        #include "math.h"
    
        double tlvar;
        //following line is needed so get_value() is not inlined by compiler
        double get_value() __attribute__ ((noinline));
        double get_value()
        {
          return tlvar;
        }
        int main()
    
        {
          int i;
          double f=0.0;
          tlvar = 1.0;
          for(i=0; i<1000000000; i++)
          {
             f += sqrt(get_value());
          }
          printf("f = %f\n", f);
          return 1;
        }
    

    这是get_value()的汇编代码

    Dump of assembler code for function get_value:
    => 0x0000000000400560 <+0>:     movsd  0x200478(%rip),%xmm0        # 0x6009e0 <tlvar>
       0x0000000000400568 <+8>:     retq
    End of assembler dump.
    

    这是它的运行速度:

    $ time ./inet_test_no_thread
    f = 1000000000.000000
    
    real    0m5.169s
    user    0m5.137s
    sys     0m0.002s
    
  2. 可执行文件中有__thread变量(不在共享库中),仍然没有缓慢

    #include "stdio.h"
    #include "math.h"
    
    __thread double tlvar;
    //following line is needed so get_value() is not inlined by compiler
    double get_value() __attribute__ ((noinline));
    double get_value()
    {
      return tlvar;
    }
    
    int main()
    {
      int i;
      double f=0.0;
    
      tlvar = 1.0;
      for(i=0; i<1000000000; i++)
      {
        f += sqrt(get_value());
      }
      printf("f = %f\n", f);
      return 1;
    }
    

    这是get_value()的汇编代码

    (gdb) disassemble get_value
    Dump of assembler code for function get_value:
    => 0x0000000000400590 <+0>:     movsd  %fs:0xfffffffffffffff8,%xmm0
       0x000000000040059a <+10>:    retq
    End of assembler dump.
    

    这是它的运行速度:

    $ time ./inet_test
    f = 1000000000.000000
    
    real    0m5.232s
    user    0m5.158s
    sys     0m0.007s
    

    因此,很明显,当__thread var 在可执行文件中时,它与普通全局变量一样快。

  3. 有一个__thread 变量,它在一个共享库中,速度很慢

    可执行文件:

    $ cat inet_test_main.c
    #include "stdio.h"
    #include "math.h"
    int test();
    
    int main()
    {
       test();
       return 1;
    }
    

    共享库:

    $ cat inet_test_lib.c
    #include "stdio.h"
    #include "math.h"
    
    static __thread double tlvar;
    //following line is needed so get_value() is not inlined by compiler
    double get_value() __attribute__ ((noinline));
    double get_value()
    {
      return tlvar;
    }
    
    int test()
    {
      int i;
      double f=0.0;
      tlvar = 1.0;
      for(i=0; i<1000000000; i++)
      {
        f += sqrt(get_value());
      }
      printf("f = %f\n", f);
      return 1;
    }
    

    这是get_value()的汇编代码,看看有什么不同——它调用__tls_get_addr()

    Dump of assembler code for function get_value:
    => 0x00007ffff7dfc6d0 <+0>:     lea    0x200329(%rip),%rdi        # 0x7ffff7ffca00
       0x00007ffff7dfc6d7 <+7>:     callq  0x7ffff7dfc5c8 <__tls_get_addr@plt>
       0x00007ffff7dfc6dc <+12>:    movsd  0x0(%rax),%xmm0
       0x00007ffff7dfc6e4 <+20>:    retq
    End of assembler dump.
    
    (gdb) disas __tls_get_addr
    Dump of assembler code for function __tls_get_addr:
       0x0000003c40a114d0 <+0>:     push   %rbx
       0x0000003c40a114d1 <+1>:     mov    %rdi,%rbx
    => 0x0000003c40a114d4 <+4>:     mov    %fs:0x8,%rdi
       0x0000003c40a114dd <+13>:    mov    0x20fa74(%rip),%rax        # 0x3c40c20f58 <_rtld_local+3928>
       0x0000003c40a114e4 <+20>:    cmp    %rax,(%rdi)
       0x0000003c40a114e7 <+23>:    jne    0x3c40a11505 <__tls_get_addr+53>
       0x0000003c40a114e9 <+25>:    xor    %esi,%esi
       0x0000003c40a114eb <+27>:    mov    (%rbx),%rdx
       0x0000003c40a114ee <+30>:    mov    %rdx,%rax
       0x0000003c40a114f1 <+33>:    shl    $0x4,%rax
       0x0000003c40a114f5 <+37>:    mov    (%rax,%rdi,1),%rax
       0x0000003c40a114f9 <+41>:    cmp    $0xffffffffffffffff,%rax
       0x0000003c40a114fd <+45>:    je     0x3c40a1151b <__tls_get_addr+75>
       0x0000003c40a114ff <+47>:    add    0x8(%rbx),%rax
       0x0000003c40a11503 <+51>:    pop    %rbx
       0x0000003c40a11504 <+52>:    retq
       0x0000003c40a11505 <+53>:    mov    (%rbx),%rdi
       0x0000003c40a11508 <+56>:    callq  0x3c40a11200 <_dl_update_slotinfo>
       0x0000003c40a1150d <+61>:    mov    %rax,%rsi
       0x0000003c40a11510 <+64>:    mov    %fs:0x8,%rdi
       0x0000003c40a11519 <+73>:    jmp    0x3c40a114eb <__tls_get_addr+27>
       0x0000003c40a1151b <+75>:    callq  0x3c40a11000 <tls_get_addr_tail>
       0x0000003c40a11520 <+80>:    jmp    0x3c40a114ff <__tls_get_addr+47>
    End of assembler dump.
    

    它的运行速度几乎慢了两倍!

    $ time ./inet_test_main
    f = 1000000000.000000
    
    real    0m9.978s
    user    0m9.906s
    sys     0m0.004s
    

    最后 - 这是perf 报告的 - __tls_get_addr - 21% 的 CPU 利用率:

    $ perf report --stdio
    #
    # Events: 10K cpu-clock
    #
    # Overhead         Command        Shared Object              Symbol
    # ........  ..............  ...................  ..................
    #
        58.05%  inet_test_main  libinet_test_lib.so  [.] test
        21.15%  inet_test_main  ld-2.12.so           [.] __tls_get_addr
        10.69%  inet_test_main  libinet_test_lib.so  [.] get_value
         5.07%  inet_test_main  libinet_test_lib.so  [.] get_value@plt
         4.82%  inet_test_main  libinet_test_lib.so  [.] __tls_get_addr@plt
         0.23%  inet_test_main  [kernel.kallsyms]    [k] 0xffffffffa0165b75
    

因此,当线程局部变量位于共享库中时(声明为静态且仅在共享库中使用),您可以看到它相当慢。如果共享库中的线程局部变量很少被访问,那么这不是性能问题。如果像在这个测试中那样经常使用它,那么开销将会很大。

cmets 中提到的文档http://www.akkadia.org/drepper/tls.pdf 讨论了四种可能的 TLS 访问模型。坦率地说,我不明白什么时候使用“Initial exec TLS model”,但是对于其他三个模型,只有当__thread变量在可执行文件中并且可以从可执行文件访问时,才有可能避免调用__tls_get_addr()

【讨论】:

  • +1 用于所有这些测试。伟大的。但是,每次操作 5 纳秒并不是我所说的非常慢。它与函数调用的顺序相同,因此除非线程局部变量实际上是您唯一要做的事情,否则它永远不会成为问题。线程同步通常要昂贵得多。如果您可以通过使用线程本地存储来避免这种情况,那么您将拥有一个巨大的双赢共享库。
  • 您可以在共享库中使用 -ftls-model=initial-exec 或 __attribute((tls_model("initial-exec"))) 但您必须非常小心。它破坏了 dlopen 并且共享库对象加载的顺序也变得很重要,因为如果已经加载了太多静态或动态 tls 对象,则设置了 STATIC_TLS 标志的 elf 可能无法加载。 (即你应该先加载静态 tls 对象)
【解决方案2】:

在 Linux 中访问线程局部变量的速度有多快

这取决于很多事情。

某些处理器 (i*86) 具有特殊段(fs,或x86_64 模式下的gs)。其他处理器没有(但通常它们会保留一个寄存器用于访问当前线程,使用该专用寄存器很容易找到TLS)。

i*86 上,使用fs,访问几乎与直接内存访问一样快。

我一直在阅读关于线程局部变量访问缓慢的恐怖故事

如果您提供一些此类恐怖故事的链接,将会有所帮助。如果没有链接,就无法判断他们的作者是否知道他们在说什么。

【讨论】:

  • 恐怖故事?没问题:我在嵌入式 MIPS 平台上工作过,每次访问线程本地存储都会导致内核调用非常慢。您可以在该平台上每秒进行大约 8000 次 TLS 访问。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-26
  • 1970-01-01
  • 1970-01-01
  • 2011-04-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多