Lợi thế của __builtin_Exect của GCC trong các câu lệnh khác là gì?


144

Tôi đã đi qua một #definetrong đó họ sử dụng __builtin_expect.

Các tài liệu nói:

Chức năng tích hợp: long __builtin_expect (long exp, long c)

Bạn có thể sử dụng __builtin_expectđể cung cấp trình biên dịch với thông tin dự đoán nhánh. Nói chung, bạn nên sử dụng phản hồi hồ sơ thực tế cho điều này ( -fprofile-arcs), vì các lập trình viên nổi tiếng là rất tệ trong việc dự đoán cách các chương trình của họ thực sự hoạt động. Tuy nhiên, có những ứng dụng mà dữ liệu này khó thu thập.

Giá trị trả về là giá trị của exp, nên là một biểu thức tích phân. Các ngữ nghĩa của tích hợp là dự kiến ​​rằng exp == c. Ví dụ:

      if (__builtin_expect (x, 0))
        foo ();

sẽ chỉ ra rằng chúng tôi không mong đợi để gọi foo, vì chúng tôi dự kiến xlà số không.

Vậy tại sao không trực tiếp sử dụng:

if (x)
    foo ();

thay vì cú pháp phức tạp với __builtin_expect?



3
Tôi nghĩ rằng mã trực tiếp của bạn nên có if ( x == 0) {} else foo();.. hoặc đơn giản if ( x != 0 ) foo();là tương đương với mã từ tài liệu GCC.
Nawaz

Câu trả lời:


187

Hãy tưởng tượng mã lắp ráp sẽ được tạo từ:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

Tôi đoán nó phải là một cái gì đó như:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

Bạn có thể thấy rằng các hướng dẫn được sắp xếp theo thứ tự sao cho bartrường hợp xảy ra trước footrường hợp (trái ngược với mã C). Điều này có thể sử dụng đường ống CPU tốt hơn, vì một bước nhảy sẽ phá vỡ các hướng dẫn đã được tìm nạp.

Trước khi bước nhảy được thực hiện, các hướng dẫn bên dưới nó ( bartrường hợp) được đẩy vào đường ống. Vì footrường hợp này là không thể, nhảy quá là không thể, do đó, đập vỡ đường ống là không thể.


1
Nó thực sự hoạt động như vậy? Tại sao định nghĩa foo không thể đến đầu tiên? Thứ tự của các định nghĩa hàm là không liên quan, theo như bạn có một nguyên mẫu, phải không?
kingsmasher1

63
Đây không phải là về định nghĩa chức năng. Đó là về việc sắp xếp lại mã máy theo cách gây ra xác suất nhỏ hơn cho CPU để tìm nạp các hướng dẫn sẽ không được thực thi.
Blagovest Buyukliev

4
Ồ tôi hiểu rồi. Vì vậy, ý bạn là vì có xác suất cao x = 0nên thanh được đưa ra trước. Và foo, được xác định sau vì cơ hội (chứ không phải xác suất sử dụng) là ít hơn, phải không?
kingsmasher1

1
À..cảm ơn. Đó là lời giải thích tốt nhất. Mã lắp ráp thực sự tạo ra mánh khóe :)
kingsmasher1

5
Điều này cũng có thể nhúng các gợi ý cho bộ dự đoán nhánh CPU , cải thiện đường ống
Hasturkun

50

Hãy dịch ngược để xem GCC 4.8 làm gì với nó

Blagovest đã đề cập đến việc đảo ngược chi nhánh để cải thiện đường ống, nhưng các trình biên dịch hiện tại có thực sự làm được không? Hãy cùng tìm hiểu!

Không có __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

Biên dịch và dịch ngược với GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Đầu ra:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

Thứ tự lệnh trong bộ nhớ không thay đổi: đầu tiên putsvà sau đó retqtrả về.

Với __builtin_expect

Bây giờ thay thế if (i)bằng:

if (__builtin_expect(i, 0))

và chúng tôi nhận được:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

Đã putsđược chuyển đến cuối của chức năng, sự retqtrở lại!

Mã mới về cơ bản giống như:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

Tối ưu hóa này đã không được thực hiện với -O0.

Nhưng chúc may mắn khi viết một ví dụ chạy nhanh __builtin_expecthơn mà không có, CPU thực sự rất thông minh ngày đó . Những nỗ lực ngây thơ của tôi đang ở đây .

C ++ 20 [[likely]][[unlikely]]

C ++ 20 đã tiêu chuẩn hóa các phần tử tích hợp C ++ này: Cách sử dụng thuộc tính có khả năng / không có khả năng của C ++ 20 trong câu lệnh if-other Họ có thể (một cách chơi chữ!) Làm điều tương tự.


1
Kiểm tra chức năng Clark_once của libdispatch, sử dụng __builtin_Exect để tối ưu hóa thực tế. Đường dẫn chậm chạy một lần và khai thác __builtin_Exect để gợi ý cho người dự đoán nhánh rằng đường dẫn nhanh nên được thực hiện. Con đường nhanh chạy mà không sử dụng bất kỳ ổ khóa nào cả! mikeash.com/pyblog/ từ
Adam Kaplan

Dường như không tạo ra bất kỳ sự khác biệt nào trong GCC 9.2: gcc.godbolt.org/z/GzP6cx (thực tế, đã có trong 8.1)
Ruslan

40

Ý tưởng __builtin_expectlà nói với trình biên dịch rằng bạn sẽ thường thấy rằng biểu thức ước lượng thành c, để trình biên dịch có thể tối ưu hóa cho trường hợp đó.

Tôi đoán rằng ai đó nghĩ rằng họ thông minh và họ đang tăng tốc mọi thứ bằng cách làm điều này.

Thật không may, trừ khi tình huống được hiểu rất rõ (có khả năng là họ đã không làm điều đó), nó cũng có thể làm mọi thứ tồi tệ hơn. Các tài liệu thậm chí nói:

Nói chung, bạn nên sử dụng phản hồi hồ sơ thực tế cho điều này ( -fprofile-arcs), vì các lập trình viên nổi tiếng là rất tệ trong việc dự đoán cách các chương trình của họ thực sự hoạt động. Tuy nhiên, có những ứng dụng mà dữ liệu này khó thu thập.

Nói chung, bạn không nên sử dụng __builtin_expecttrừ khi:

  • Bạn có một vấn đề hiệu suất rất thực tế
  • Bạn đã tối ưu hóa các thuật toán trong hệ thống một cách thích hợp
  • Bạn đã có dữ liệu hiệu suất để sao lưu xác nhận của mình rằng một trường hợp cụ thể có khả năng nhất

7
@Michael: Đó không thực sự là một mô tả về dự đoán chi nhánh.
Oliver Charlesworth

3
"Hầu hết các lập trình viên là BAD" hoặc dù sao cũng không tốt hơn trình biên dịch. Bất kỳ kẻ ngốc nào cũng có thể nói rằng trong một vòng lặp for, điều kiện tiếp tục có khả năng là đúng, nhưng trình biên dịch cũng biết điều đó nên không có lợi ích gì khi nói điều đó. Nếu vì lý do nào đó bạn đã viết một vòng lặp gần như luôn luôn bị hỏng ngay lập tức và nếu bạn không thể cung cấp dữ liệu hồ sơ cho trình biên dịch cho PGO, thì có lẽ lập trình viên biết điều gì đó trình biên dịch không.
Steve Jessop

15
Trong một số tình huống, không quan trọng chi nhánh nào có khả năng hơn, mà là chi nhánh nào quan trọng. Nếu nhánh bất ngờ dẫn đến hủy bỏ (), thì khả năng không thành vấn đề và nhánh được mong đợi sẽ được ưu tiên hiệu suất khi tối ưu hóa.
Neowizard

1
Vấn đề với yêu cầu của bạn là việc tối ưu hóa CPU có thể thực hiện liên quan đến xác suất nhánh bị giới hạn khá nhiều đối với một: dự đoán nhánh và tối ưu hóa này xảy ra cho dù bạn có sử dụng __builtin_expecthay không . Mặt khác, trình biên dịch có thể thực hiện nhiều tối ưu hóa dựa trên xác suất nhánh, chẳng hạn như tổ chức mã sao cho đường dẫn nóng tiếp giáp, mã di chuyển không có khả năng được tối ưu hóa hơn nữa hoặc giảm kích thước của nó, đưa ra quyết định về nhánh nào để vector hóa, lập kế hoạch tốt hơn các con đường nóng, và như vậy.
BeeOnRope

1
... Không có thông tin từ nhà phát triển, nó bị mù và chọn một chiến lược trung lập. Nếu nhà phát triển nói đúng về xác suất (và trong nhiều trường hợp, việc hiểu rằng một nhánh thường được lấy / không lấy) là điều không quan trọng - bạn sẽ nhận được những lợi ích này. Nếu bạn không nhận được một số hình phạt, nhưng nó không lớn hơn nhiều so với lợi ích và quan trọng nhất, không ai trong số này bằng cách nào đó ghi đè lên dự đoán nhánh CPU.
BeeOnRope

13

Vâng, như đã nói trong phần mô tả, phiên bản đầu tiên thêm một yếu tố dự đoán vào cấu trúc, nói với trình biên dịch rằng x == 0nhánh có nhiều khả năng - đó là nhánh sẽ được chương trình của bạn lấy thường xuyên hơn.

Với ý nghĩ đó, trình biên dịch có thể tối ưu hóa điều kiện để nó yêu cầu số lượng công việc ít nhất khi điều kiện dự kiến ​​giữ, với chi phí có thể phải làm nhiều công việc hơn trong trường hợp điều kiện không mong muốn.

Hãy xem cách các điều kiện được thực hiện trong giai đoạn biên dịch, và cả trong phần kết quả, để xem làm thế nào một nhánh có thể làm việc ít hơn các nhánh khác.

Tuy nhiên, tôi chỉ mong muốn tối ưu hóa này có hiệu quả rõ rệt nếu điều kiện trong câu hỏi là một phần của vòng lặp bên trong chặt chẽ được gọi là nhiều , vì sự khác biệt trong mã kết quả là tương đối nhỏ. Và nếu bạn tối ưu hóa nó sai cách, bạn cũng có thể giảm hiệu suất của mình.


Nhưng cuối cùng, tất cả chỉ là kiểm tra điều kiện của trình biên dịch, bạn có muốn nói rằng trình biên dịch luôn giả định nhánh này và tiến hành, và sau đó nếu không có sự trùng khớp thì sao? Chuyện gì xảy ra Tôi nghĩ rằng có một cái gì đó nhiều hơn về công cụ dự đoán chi nhánh này trong thiết kế trình biên dịch, và cách nó hoạt động.
kingsmasher1

2
Đây thực sự là một tối ưu hóa vi mô. Tìm kiếm cách các điều kiện được thực hiện, có một sự thiên vị nhỏ đối với một chi nhánh. Như một ví dụ giả thuyết, giả sử một điều kiện trở thành một bài kiểm tra cộng với một bước nhảy trong lắp ráp. Sau đó, nhánh nhảy chậm hơn so với nhánh không nhảy, vì vậy bạn muốn biến nhánh dự kiến ​​thành nhánh không nhảy.
Kerrek SB

Cảm ơn, bạn và Michael tôi nghĩ có quan điểm tương tự nhưng đặt các từ khác nhau :-) Tôi hiểu nội dung trình biên dịch chính xác về Test-and-Branch không thể giải thích ở đây :)
kingsmasher1

Chúng cũng rất dễ tìm hiểu bằng cách tìm kiếm trên internet :-)
Kerrek SB

Tôi tốt hơn nên quay lại cuốn sách đại học của tôi về compiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

Tôi không thấy bất kỳ câu trả lời nào giải quyết câu hỏi mà tôi nghĩ bạn đang hỏi, diễn giải:

Có một cách di động hơn để dự đoán nhánh dự đoán cho trình biên dịch.

Tiêu đề của câu hỏi của bạn làm tôi nghĩ làm theo cách này:

if ( !x ) {} else foo();

Nếu trình biên dịch giả định rằng 'true' có nhiều khả năng, nó có thể tối ưu hóa cho việc không gọi foo().

Vấn đề ở đây chỉ là bạn nói chung, không biết trình biên dịch sẽ giả định điều gì - vì vậy, bất kỳ mã nào sử dụng loại kỹ thuật này sẽ cần phải được đo lường cẩn thận (và có thể được theo dõi theo thời gian nếu bối cảnh thay đổi).


Trên thực tế, điều này có thể chính xác là những gì OP dự định gõ (như được chỉ định bởi tiêu đề) - nhưng vì một số lý do, việc sử dụng elsebị bỏ lại ngoài cơ thể của bài đăng.
Brent Bradburn

1

Tôi kiểm tra nó trên Mac theo @Blagovest Buyukliev và @Ciro. Các hội đồng nhìn rõ ràng và tôi thêm ý kiến;

Các lệnh là gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

Khi tôi sử dụng -O3, nó trông giống nhau cho dù __builtin_Exect (i, 0) có tồn tại hay không.

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

Khi biên dịch với -O2, nó trông khác và không có __builtin_Exect (i, 0)

Đầu tiên không có

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

Bây giờ với __builtin_Exect (i, 0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

Để tóm tắt, __builtin_Exect hoạt động trong trường hợp cuối cùng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.