2012-03-28 9 views
17

Quanto è veloce l'accesso alle variabili locali di un thread in Linux. Dal codice generato dal compilatore gcc, posso vedere che utilizza il registro dei segmenti fs. Quindi apparentemente, l'accesso alla variabile locale del thread non dovrebbe costare cicli extra.Quanto è veloce l'accesso alle variabili locali dei thread su Linux

Tuttavia, continuo a leggere storie dell'orrore sulla lentezza dell'accesso alle variabili locali del thread. Come mai? Certo, a volte compilatori diversi usano un approccio diverso rispetto al registro del segmento fs, ma accede alla variabile locale del thread anche attraverso il registro del segmento fs?

+5

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 –

+0

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. –

+2

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

risposta

9

Quanto è veloce l'accesso a un filo variabili locali in Linux

Dipende, su un sacco di cose.

Alcuni processori (i*86) hanno un segmento speciale (fs o gs in modalità x86_64). Altri processori no (ma di solito avranno un registro riservato per accedere al thread corrente, e TLS è facile da trovare usando quel registro dedicato).

Su i*86, utilizzando fs, l'accesso è quasi veloce come accesso diretto alla memoria.

io continuo a leggere storie di orrore circa la lentezza del filo locale Accessibile variabile

Avrebbe aiutato se hai fornito link ad alcuni di questi racconti dell'orrore. Senza i collegamenti, è impossibile dire se i loro autori sanno di cosa stanno parlando.

+0

Storie dell'orrore? Nessun problema: ho lavorato su una piattaforma MIPS incorporata in cui ogni accesso alla memoria locale del thread ha comportato una chiamata al kernel molto lenta.Potresti fare approssimativamente 8000 accessi TLS al secondo su quella piattaforma. –

12

Tuttavia, continuo a leggere storie dell'orrore sulla lentezza dell'accesso alle variabili locali del thread. Come mai?

Lasciami dimostrare la lentezza della variabile locale del thread su Linux x86_64 con un esempio che ho preso da http://software.intel.com/en-us/blogs/2011/05/02/the-hidden-performance-cost-of-accessing-thread-local-variables.

  1. 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 
    
  2. 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.

  3. 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 
    

Quindi, come si può vedere quando una variabile locale thread è in una libreria condivisa (dichiarata statica e utilizzata solo in una libreria condivisa) è piuttosto lenta. Se si accede raramente a una variabile locale del thread in una libreria condivisa, non si tratta di un problema per performace. Se viene usato abbastanza spesso come in questo test, il sovraccarico sarà significativo.

Il documento http://www.akkadia.org/drepper/tls.pdf menzionato nei commenti parla di quattro possibili modelli di accesso TLS. Sinceramente, non capisco quando viene utilizzato "Initial exec TLS model", ma come per gli altri tre modelli è possibile evitare di chiamare __tls_get_addr() solo quando la variabile __thread è in un eseguibile e si accede dall'eseguibile.

+0

+1 per tutti questi test. Grande. Tuttavia, cinque nanosecondi per operazione non sono quelli che definirei molto lenti. È nello stesso ordine di una chiamata di funzione, quindi a meno che le variabili locali del thread non siano praticamente l'unica cosa che fai, non dovrebbe mai essere un problema. La sincronizzazione dei thread è generalmente molto più costosa. E se puoi evitarlo usando lo storage locale thread, hai una win win - libreria condivisa o meno. – cmaster

Problemi correlati