c - pthread - set environment variable linux
¿Qué tan rápido es el hilo variable de acceso local en Linux (2)
¿Qué tan rápido es acceder a las variables locales de un hilo en Linux?
Depende de muchas cosas.
Algunos procesadores ( i*86
) tienen un segmento especial ( fs
, o gs
en modo x86_64
). Otros procesadores no lo hacen (pero generalmente tendrán un registro reservado para acceder al subproceso actual, y es fácil encontrar el TLS
utilizando ese registro dedicado).
En i*86
, utilizando fs
, el acceso es casi tan rápido como el acceso directo a la memoria.
Sigo leyendo historias de terror sobre la lentitud del acceso a variables locales de subprocesos
Le hubiera ayudado si proporcionara enlaces a algunas de esas historias de horror. Sin los enlaces, es imposible saber si sus autores saben de qué están hablando.
¿Qué tan rápido es acceder a un hilo de variables locales en Linux. Del código generado por el compilador gcc, puedo ver que se utiliza el registro de segmento fs
. Entonces, aparentemente, el acceso a la variable local del hilo no debería costar ciclos adicionales.
Sin embargo, sigo leyendo historias de horror sobre la lentitud del acceso a las variables locales. ¿Cómo? Claro, a veces, los compiladores diferentes utilizan un enfoque diferente al uso del registro de segmento fs
, pero ¿el acceso a la variable local del subproceso a través del registro de segmento fs
también es lento?
Sin embargo, sigo leyendo historias de horror sobre la lentitud del acceso a las variables locales. ¿Cómo?
Permítame demostrar la lentitud de la variable local del hilo en Linux x86_64 con un ejemplo que he tomado de http://software.intel.com/en-us/blogs/2011/05/02/the-hidden-performance-cost-of-accessing-thread-local-variables .
No
__thread
variable__thread
, no hay lentitud .Usaré el rendimiento de esta prueba como base.
#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; }
Este es el código ensamblador de 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.
Esto es lo rápido que corre:
$ time ./inet_test_no_thread f = 1000000000.000000 real 0m5.169s user 0m5.137s sys 0m0.002s
Existe
__thread
variable__thread
en un ejecutable (no en una biblioteca compartida), aún sin lentitud .#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; }
Este es el código ensamblador de 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.
Esto es lo rápido que corre:
$ time ./inet_test f = 1000000000.000000 real 0m5.232s user 0m5.158s sys 0m0.007s
Entonces, es bastante obvio que cuando
__thread
var está en el ejecutable es tan rápido como una variable global ordinaria.Hay una variable
__thread
y está en una biblioteca compartida, hay lentitud .Ejecutable:
$ cat inet_test_main.c #include "stdio.h" #include "math.h" int test(); int main() { test(); return 1; }
Biblioteca compartida:
$ 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; }
Este es el código ensamblador de get_value (), vea qué tan diferente es, llama a
__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.
¡Funciona casi dos veces más lento! :
$ time ./inet_test_main f = 1000000000.000000 real 0m9.978s user 0m9.906s sys 0m0.004s
Y, finalmente, esto es lo que informa
perf
- __tls_get_addr - 21% de la utilización de la 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
Entonces, como puede ver cuando una variable local de subproceso está en una biblioteca compartida (declarada estática y usada solo en una biblioteca compartida) es bastante lenta . Si rara vez se accede a una variable local de subproceso en una biblioteca compartida, entonces no es un problema para el rendimiento. Si se usa con bastante frecuencia como en esta prueba, la sobrecarga será significativa.
El documento akkadia.org/drepper/tls.pdf que se menciona en los comentarios habla sobre cuatro posibles modelos de acceso TLS. Francamente, no comprendo cuándo se usa el "Modelo de ejecución inicial TLS", pero en cuanto a los otros tres modelos, es posible evitar llamar a __tls_get_addr()
solo cuando la variable __thread
está en un ejecutable y se accede desde el ejecutable.