2012-03-28 32 views
17

Nhanh như thế nào khi truy cập vào một biến địa phương luồng trong Linux. Từ mã được tạo bởi trình biên dịch gcc, tôi có thể thấy đó là sử dụng thanh ghi phân đoạn fs. Vì vậy, rõ ràng, truy cập vào biến địa phương thread không nên chi phí thêm chu kỳ.Truy cập biến cục bộ luồng nhanh trên Linux

Tuy nhiên, tôi tiếp tục đọc những câu chuyện kinh dị về sự chậm trễ của truy cập biến cục bộ của chuỗi. Làm thế nào mà? Chắc chắn, đôi khi các trình biên dịch khác nhau sử dụng một cách tiếp cận khác với việc sử dụng thanh ghi phân đoạn fs, nhưng đang truy cập biến cục bộ thông qua thanh ghi phân đoạn fs cũng chậm?

+5

những gì đang xảy ra đằng sau hậu trường: http://www.akkadia.org/drepper/tls.pdf .. có ai cảm thấy động cơ để đọc điều này và tóm tắt câu trả lời trong câu trả lời ngắn không? : D –

+0

"Câu chuyện kinh dị" có lẽ là từ TSS (Chủ đề lưu trữ cụ thể) thông qua pthreads_setspecific. TSS chậm hơn TLS, nhưng nếu được thực hiện đúng cách không phải là toàn bộ. –

+2

Tôi có thể cung cấp cho bạn một câu chuyện kinh dị về sự chậm chạp của biến cục bộ _non_ thread (bộ đếm số nguyên đơn giản), được sửa đổi thông qua một số luồng và làm chậm hệ thống xuống để thu thập thông tin do bộ nhớ cache bị rình mò. Làm cho nó thread địa phương và làm một tổng kết của tất cả các địa phương thread ở cuối đã cho tôi một tăng tốc của một yếu tố 100 hoặc tương tự. – hirschhornsalz

Trả lời

9

nhanh như thế nào truy cập vào một chủ đề biến địa phương trong Linux

Nó phụ thuộc, trên rất nhiều thứ.

Một số bộ xử lý (i*86) có phân đoạn đặc biệt (fs hoặc gs ở chế độ x86_64). Các bộ vi xử lý khác không (nhưng thường thì chúng sẽ có một thanh ghi dành riêng cho việc truy cập vào chuỗi hiện tại, và TLS dễ dàng tìm thấy bằng cách sử dụng thanh ghi chuyên dụng đó).

Trên i*86, sử dụng fs, quyền truy cập là gần như nhanh như truy cập bộ nhớ trực tiếp.

Tôi tiếp tục đọc câu chuyện kinh dị về sự chậm chạp của thread địa phương truy cập biến

Nó sẽ giúp nếu bạn cung cấp các liên kết đến một số câu chuyện kinh dị như vậy. Nếu không có các liên kết, không thể biết liệu tác giả của họ có biết họ đang nói về điều gì không.

+0

Câu chuyện kinh dị? Không có vấn đề gì: Tôi đã làm việc trên một nền tảng MIPS nhúng, trong đó mỗi truy cập vào luồng lưu trữ cục bộ luồng dẫn đến một cuộc gọi hạt nhân rất chậm.Bạn có thể thực hiện khoảng 8000 lần truy cập TLS mỗi giây trên nền tảng đó. –

12

Tuy nhiên, tôi tiếp tục đọc những câu chuyện kinh dị về sự chậm trễ của truy cập biến cục bộ của chuỗi. Làm thế nào mà?

Hãy để tôi chứng minh sự chậm trễ của biến cục bộ luồng trên Linux x86_64 với ví dụ tôi lấy từ http://software.intel.com/en-us/blogs/2011/05/02/the-hidden-performance-cost-of-accessing-thread-local-variables.

  1. Không __thread biến, không chậm.

    Tôi sẽ sử dụng hiệu suất của thử nghiệm này làm cơ sở.

    #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; 
        } 
    

    Đây là mã lắp ráp của 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. 
    

    Đây là nhanh như thế nào nó chạy:

    $ time ./inet_test_no_thread 
    f = 1000000000.000000 
    
    real 0m5.169s 
    user 0m5.137s 
    sys  0m0.002s 
    
  2. __thread biến trong một thực thi (không có trong thư viện chia sẻ) , vẫn không có sự chậm trễ.

    #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; 
    } 
    

    Đây là mã lắp ráp của 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. 
    

    Đây là nhanh như thế nào nó chạy:

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

    Vì vậy, nó là khá rõ ràng rằng khi __thread var là trong thực thi nó nhanh như biến toàn cục thông thường.

  3. Có biến số __thread và nằm trong thư viện được chia sẻ, có độ trễ.

    Executable:

    $ cat inet_test_main.c 
    #include "stdio.h" 
    #include "math.h" 
    int test(); 
    
    int main() 
    { 
        test(); 
        return 1; 
    } 
    

    Thư viện dùng chung:

    $ 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; 
    } 
    

    Đây là mã lắp ráp của get_value(), xem cách khác nhau đó là - nó gọi __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. 
    

    Nó chạy gần gấp đôi!:

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

    Và cuối cùng - đây là những gì perf báo cáo - __tls_get_addr - 21% sử dụng 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 
    

Vì vậy, như bạn có thể nhìn thấy khi một biến địa phương chủ đề là trong một thư viện được chia sẻ (được khai báo tĩnh và chỉ được sử dụng trong một thư viện được chia sẻ) nó khá chậm. Nếu một biến cục bộ thread trong một thư viện được chia sẻ hiếm khi được truy cập, thì nó không phải là một vấn đề đối với performace. Nếu nó được sử dụng khá thường xuyên như trong bài kiểm tra này thì chi phí sẽ rất đáng kể.

Tài liệu http://www.akkadia.org/drepper/tls.pdf được đề cập trong các cuộc thảo luận về bốn mô hình truy cập TLS có thể có. Thành thật mà nói, tôi không hiểu khi "mô hình TLS exec ban đầu" được sử dụng, nhưng đối với ba mô hình khác, có thể tránh gọi __tls_get_addr() chỉ khi biến số __thread ở trong tệp thực thi và được truy cập từ tệp thực thi.

+0

+1 cho tất cả thử nghiệm này. Tuyệt quá. Tuy nhiên, năm nano giây cho mỗi hoạt động không phải là những gì tôi sẽ gọi thực sự chậm. Nó theo thứ tự như một cuộc gọi hàm, vì vậy trừ khi các biến thread-local hầu như là điều duy nhất bạn làm, nó không bao giờ là vấn đề. Đồng bộ hóa chủ đề thường đắt hơn nhiều. Và nếu bạn có thể tránh điều đó bằng cách sử dụng bộ nhớ cục bộ, bạn có một thư viện chia sẻ rất lớn hoặc không. – cmaster

Các vấn đề liên quan