Насколько быстрый доступ к локальной переменной потока в Linux - PullRequest
17 голосов
/ 28 марта 2012

Как быстро осуществляется доступ к локальным переменным потока в Linux. Из кода, сгенерированного компилятором gcc, я вижу, что он использует регистр сегмента fs. Очевидно, что доступ к локальной переменной потока не должен стоить лишних циклов.

Однако я продолжаю читать ужасные истории о медленном доступе к локальным переменным потока. Как так? Конечно, иногда разные компиляторы используют другой подход, чем использование fs сегментного регистра, но медленный ли доступ к локальной переменной потока через fs сегментный регистр тоже?

Ответы [ 2 ]

13 голосов
/ 25 августа 2014

Однако я продолжаю читать ужасные истории о медленном доступе к локальным переменным потока. Как получилось?

Позвольте мне продемонстрировать медлительность локальной переменной потока в Linux x86_64 на примере, который я взял из http://software.intel.com/en-us/blogs/2011/05/02/the-hidden-performance-cost-of-accessing-thread-local-variables.

  1. Нет __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
    
  2. В исполняемом файле (не в общей библиотеке) есть переменная __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 находится в исполняемом файле, он работает так же быстро, как обычная глобальная переменная.

  3. Существует переменная __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 - 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
    

Итак, как вы можете видеть, когда локальная переменная потока находится в общей библиотеке (объявлена ​​статической и используется только в общей библиотеке) это довольно медленно . Если локальная переменная потока в разделяемой библиотеке доступна редко, то это не проблема для производительности. Если он используется довольно часто, как в этом тесте, то издержки будут значительными.

В документе http://www.akkadia.org/drepper/tls.pdf, который упоминается в комментариях, говорится о четырех возможных моделях доступа TLS. Честно говоря, я не понимаю, когда используется «Initial exec TLS model», но что касается других трех моделей, можно избежать вызова __tls_get_addr() только тогда, когда переменная __thread находится в исполняемом файле и доступна из исполняемого файла.

9 голосов
/ 28 марта 2012

Как быстро осуществляется доступ к локальным переменным потока в Linux

Это зависит от многих вещей.

Некоторые процессоры (i*86) имеют специальный сегмент (fs или gs в режиме x86_64). Другие процессоры этого не делают (но обычно они имеют регистр, зарезервированный для доступа к текущему потоку, и TLS легко найти с помощью этого специального регистра).

На i*86, используя fs, доступ на почти такой же быстрый, как и прямой доступ к памяти.

Я продолжаю читать ужасные истории о медленном доступе к локальной переменной потока

Было бы полезно, если бы вы предоставили ссылки на такие ужасные истории. Без ссылок невозможно определить, знают ли их авторы, о чем они говорят.

...