在Linux中访问线程局部variables的速度有多快。 从gcc编译器生成的代码中,我可以看到使用了fs
段寄存器。 显然,访问线程局部variables不应该花费额外的周期。
不过,我一直在阅读关于线程局部variables访问速度慢的恐怖故事。 怎么来的? 当然,有时候不同的编译器使用不同的方法,而不是使用fs
段寄存器,而是通过fs
段寄存器访问线程局部variables呢?
在Linux中访问线程局部变量的速度有多快
这取决于很多事情。
某些处理器( i*86
)具有特殊的段( fs
或gs
在x86_64
模式下)。 其他处理器不(但通常他们将有一个寄存器保留访问当前线程,并且TLS
很容易找到使用该专用寄存器)。
在i*86
,使用fs
,访问速度几乎与直接内存访问一样快。
我一直在阅读有关线程局部变量访问缓慢的恐怖故事
如果你提供了一些这样的恐怖故事的链接,这将有所帮助。 没有这些链接,就不可能知道他们的作者是否知道他们在谈论什么。
不过,我一直在阅读关于线程局部变量访问速度慢的恐怖故事。 怎么来的?
让我演示Linux x86_64上的线程局部变量的缓慢度,我从http://software.intel.com/zh-cn/blogs/2011/05/02/the-hidden-performance-cost-of -accessing-thread-local-variables 。
没有__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
在可执行文件(不在共享库中)有__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在可执行文件中时,它和普通的全局变量一样快。
有一个__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 – CPU利用率的21%:
$ 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
所以,正如你所看到的,当一个线程局部变量在一个共享库中(声明为static并且只在一个共享库中使用), 它是相当慢的 。 如果共享库中的线程局部变量很少被访问,那么对于执行来说就不是问题。 如果像这个测试那样经常使用,那么开销会很大。
评论中提到的文档http://www.akkadia.org/drepper/tls.pdf提到了四种可能的TLS访问模式。 坦率地说,我不明白什么时候使用“Initial exec TLS model”,但是对于其他三种模型,只有当__thread
变量位于可执行文件中并且从可执行文件访问时, __tls_get_addr()
可能避免调用__tls_get_addr()
。