No __thread
variabile, senza lentezza.
Utilizzerò le prestazioni di questo test come 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;
}
Questo è il codice assembler di 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.
Questo è quanto velocemente si corre:
$ time ./inet_test_no_thread
f = 1000000000.000000
real 0m5.169s
user 0m5.137s
sys 0m0.002s
C'è __thread
variabile in un file eseguibile (non in libreria condivisa) , ancora senza lentezza.
#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;
}
Questo è il codice assembler di 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.
Questo è quanto velocemente si corre:
$ time ./inet_test
f = 1000000000.000000
real 0m5.232s
user 0m5.158s
sys 0m0.007s
Quindi, è del tutto evidente che quando __thread
var è nel file eseguibile è veloce come la variabile globale ordinaria.
C'è una variabile __thread
ed è in una libreria condivisa, c'è la lentezza.
eseguibile:
$ cat inet_test_main.c
#include "stdio.h"
#include "math.h"
int test();
int main()
{
test();
return 1;
}
Libreria condivisa:
$ 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;
}
Questo è il codice assembler di Get_Value(), vedere come sia diverso - chiama __tls_get_addr()
:
Dump of assembler code for function get_value:
=> 0x00007ffff7dfc6d0 <+0>: lea 0x200329(%rip),%rdi # 0x7ffff7ffca00
0x00007ffff7dfc6d7 <+7>: callq 0x7ffff7dfc5c8 <[email protected]>
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.
Viene eseguito quasi due volte più lentamente!:
$ time ./inet_test_main
f = 1000000000.000000
real 0m9.978s
user 0m9.906s
sys 0m0.004s
E infine - questo è ciò che perf
rapporti - __tls_get_addr - il 21% di utilizzo della 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 [.] [email protected]
4.82% inet_test_main libinet_test_lib.so [.] [email protected]
0.23% inet_test_main [kernel.kallsyms] [k] 0xffffffffa0165b75
cosa succede dietro le quinte: http://www.akkadia.org/drepper/tls.pdf .. qualcuno si sente motivato a leggere questo e riassumerlo in una risposta breve? : D –
Le "storie dell'orrore" sono probabilmente da TSS (Thread Specific Storage) tramite pthreads_setspecific. TSS è più lento di TLS, ma se fatto correttamente non da un intero lotto. –
Potrei darvi una storia horror della lentezza di una variabile locale _non_ thread (un contatore intero semplice), che è stata modificata attraverso diversi thread e ha rallentato il sistema fino a una ricerca per indicizzazione a causa dello snooping della cache. Rendere il thread locale e fare una sommatoria di tutti i thread locali alla fine mi ha dato un aumento di un fattore di 100 o simile. – hirschhornsalz