2012-03-28 59 views
17

Linux中訪問線程局部變量的速度有多快。從gcc編譯器生成的代碼中,我可以看到使用了fs段寄存器。顯然,訪問線程局部變量不應該花費額外的週期。Linux上的線程局部變量訪問速度有多快

但是,我一直在閱讀關於線程局部變量訪問緩慢的恐怖故事。怎麼來的?當然,有時不同的編譯器會使用與使用段寄存器fs不同的方法,但是通過fs段寄存器訪問線程局部變量也是慢的嗎?

+5

幕後發生了什麼:http://www.akkadia.org/drepper/tls.pdf ..有沒有人覺得讀這篇文章的動機,並在簡短的回答中總結一下? :D – 2012-03-28 15:53:05

+0

「恐怖故事」可能來自TSS(線程專有存儲),通過pthreads_setspecific。 TSS比TLS慢,但如果正確完成則不是很多。 – 2012-03-28 19:57:22

+2

我可以給你一個關於_non_線程局部變量(一個簡單的整數計數器)緩慢的恐怖故事,它是通過多個線程修改的,並且由於緩存監聽而使系統減慢到爬行。讓它成爲本地線程,並在最後對所有線程局部人物進行求和,這使得我的加速因子達到了100或類似。 – hirschhornsalz 2012-03-29 08:53:26

回答

9

如何快速在Linux中

正在訪問線程局部變量這取決於,在很多事情。

某些處理器(i*86)具有特殊段(fs或模式)。其他處理器不會(但通常他們將有一個保留用於訪問當前線程的寄存器,並且使用該專用寄存器很容易找到TLS)。

i*86上,使用fs,訪問是幾乎與直接存儲器訪問一樣快。

我一直在閱讀有關的線程局部變量的訪問

如果您提供鏈接到一些這樣的恐怖故事,這將有助於緩慢的恐怖故事。沒有這些鏈接,就不可能說出他們的作者是否知道他們在談論什麼。

+0

恐怖故事?沒問題:我在一個嵌入式MIPS平臺上工作,每個對線程本地存儲的訪問都導致了非常慢的內核調用。您可以在該平臺上每秒執行大約8000次TLS訪問。 – 2014-08-25 08:09:40

12

但是,我一直在閱讀有關線程局部變量訪問緩慢的恐怖故事。怎麼來的?

讓我演示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 <[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. 
    

    它運行速度差不多慢兩倍!

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

    最後 - 這就是perf報告 - __tls_get_addr - CPU利用率爲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 [.] [email protected] 
        4.82% inet_test_main libinet_test_lib.so [.] [email protected] 
        0.23% inet_test_main [kernel.kallsyms] [k] 0xffffffffa0165b75 
    

所以,你可以看到,當一個線程局部變量是在共享庫(聲明爲靜態並僅用於共享庫)它比較慢。如果一個共享庫中的線程局部變量很少被訪問,那麼這對性能來說不是問題。如果它在這個測試中經常使用,那麼開銷會很大。

在評論中提到的文檔http://www.akkadia.org/drepper/tls.pdf討論了四種可能的TLS訪問模型。坦率地說,我不明白什麼時候使用「Initial exec TLS model」,但是對於其他三種型號,只有當__thread變量位於可執行文件中並且從可執行文件訪問時,纔有可能避免調用__tls_get_addr()

+0

所有這些測試。大。然而,每次操作五納秒不是我所說的非常慢。它的順序與函數調用的順序相同,所以除非線程局部變量實際上是您所做的唯一事情,否則它不應該成爲問題。線程同步通常要昂貴得多。如果你可以通過使用線程本地存儲來避免這種情況,那麼你有一個巨大的共贏庫。 – cmaster 2014-08-25 16:48:13