2012-03-28 12 views
17

Qué tan rápido es acceder a las variables locales de un subproceso en Linux. Del código generado por el compilador gcc, puedo ver que se usa el registro de segmento fs. Entonces, aparentemente, el acceso a la variable local thread no debería costar ciclos extra.Cuán rápido es el acceso variable local de subprocesos en Linux

Sin embargo, sigo leyendo historias de terror sobre la lentitud del acceso de variable local de subprocesos. ¿Cómo? Claro, a veces los diferentes compiladores usan un enfoque diferente al de usar el registro de segmento fs, pero también está accediendo a la variable local de subproceso a través del registro de segmento fs lento?

+5

lo que está sucediendo entre bastidores: http://www.akkadia.org/drepper/tls.pdf ... ¿Alguien tiene motivos para leer esto y resumirlo en una respuesta corta? : D –

+0

Las "historias de terror" son probablemente de TSS (Thread Specific Storage) a través de pthreads_setspecific. TSS es más lento que TLS, pero si se hace correctamente no por mucho. –

+2

Podría contarte una historia de terror sobre la lentitud de una variable local _non_ thread (un contador de enteros simple), que se modificó mediante varios subprocesos y ralentizó el sistema hasta el rastreo debido al espionaje de la memoria caché. Hacer que se enrutara localmente y hacer una suma de todas las conversaciones locales al final me daba una aceleración de un factor de 100 o similar. – hirschhornsalz

Respuesta

9

¿Qué tan rápido está accediendo a un hilo de variables locales en Linux

Depende, sobre un montón de cosas.

Algunos procesadores (i*86) tienen segmento especial (fs, o gs en x86_64 modo). Otros procesadores no (pero generalmente tendrán un registro reservado para acceder al hilo actual, y TLS es fácil de encontrar usando ese registro dedicado).

En i*86, utilizando fs, el acceso es casi tan rápido como acceso directo a memoria.

sigo leyendo historias de horror acerca de la lentitud del hilo de acceso variable local

Hubiera ayudado si ya ha proporcionado enlaces a algunas de estas historias de terror. Sin los enlaces, es imposible saber si sus autores saben de lo que están hablando.

+0

Historias de terror? No hay problema: he trabajado en una plataforma MIPS incrustada donde cada acceso al almacenamiento local de subprocesos resulta en una llamada al kernel muy lenta.Podría hacer aproximadamente 8000 accesos de TLS por segundo en esa plataforma. –

12

Sin embargo, sigo leyendo historias de terror sobre la lentitud del acceso de variable local de subprocesos. ¿Cómo?

Déjenme demostrar la lentitud de la variable local de subprocesos 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.

  1. Sin __thread variables, sin lentitud.

    Voy a utilizar 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. 
    

    Esta es la rapidez con que se ejecuta:

    $ time ./inet_test_no_thread 
    f = 1000000000.000000 
    
    real 0m5.169s 
    user 0m5.137s 
    sys  0m0.002s 
    
  2. Hay __thread variable en un archivo ejecutable (no en la biblioteca compartida) , todavía 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. 
    

    Esta es la rapidez con que se ejecuta:

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

    Por lo tanto, es bastante obvio que cuando __thread var está en el ejecutable es tan rápido como la variable global ordinaria.

  3. Existe una variable __thread y se encuentra 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(), ver lo diferente que es - que llama __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. 
    

    ¡Corre casi dos veces más lento!:

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

    Y, por último - esto es lo que perf informes - __tls_get_addr - 21% de 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 [.] [email protected] 
        4.82% inet_test_main libinet_test_lib.so [.] [email protected] 
        0.23% inet_test_main [kernel.kallsyms] [k] 0xffffffffa0165b75 
    

Por lo tanto, como se puede ver cuando una variable local de hilo se encuentra en una biblioteca compartida (declarada estática y utilizada solo en una biblioteca compartida) es bastante lenta. Si rara vez se accede a una variable local de subprocesos en una biblioteca compartida, entonces no es un problema para el rendimiento. Si se usa con frecuencia como en esta prueba, la sobrecarga será significativa.

El documento http://www.akkadia.org/drepper/tls.pdf que se menciona en los comentarios habla de cuatro posibles modelos de acceso TLS. Francamente, no entiendo cuándo se usa el "modelo ejecutivo TLS inicial", pero en cuanto a los otros tres modelos, es posible evitar llamar al __tls_get_addr() solo cuando la variable __thread está en un archivo ejecutable y se accede desde el archivo ejecutable.

+0

+1 para todas estas pruebas. Estupendo. Sin embargo, cinco nanosegundos por operación no es lo que llamaría realmente lento. Está en el mismo orden que una llamada a función, por lo tanto, a menos que las variables locales de subprocesos sean prácticamente lo único que se haga, nunca debería ser un problema. La sincronización de subprocesos generalmente es mucho más costosa. Y si puede evitar eso mediante el uso de almacenamiento local de subprocesos, tiene una gran ventaja: biblioteca compartida o no. – cmaster

Cuestiones relacionadas