ข้อได้เปรียบของ __builtin_expect ของ GCC คืออะไรถ้ามีข้อความอื่น?


144

ฉันมาข้ามในที่ที่พวกเขาใช้#define__builtin_expect

เอกสารกล่าวว่า:

ฟังก์ชั่นในตัว: long __builtin_expect (long exp, long c)

คุณอาจใช้__builtin_expectเพื่อให้ข้อมูลการทำนายสาขา โดยทั่วไปคุณควรใช้ความคิดเห็นโปรไฟล์จริงสำหรับสิ่งนี้ ( -fprofile-arcs) เนื่องจากโปรแกรมเมอร์ไม่ดีพอที่จะทำนายว่าโปรแกรมของพวกเขาทำงานได้จริงอย่างไร อย่างไรก็ตามมีแอพพลิเคชั่นที่รวบรวมข้อมูลนี้ยาก

ค่าส่งคืนคือค่าของexpซึ่งควรเป็นนิพจน์ที่สำคัญ exp == cความหมายของในตัวที่ว่ามันเป็นที่คาดว่า ตัวอย่างเช่น:

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

จะระบุว่าเราไม่ได้คาดหวังว่าจะโทรfooเพราะเราคาดว่าxจะเป็นศูนย์

ดังนั้นทำไมไม่ใช้โดยตรง:

if (x)
    foo ();

แทนไวยากรณ์ที่ซับซ้อนด้วย__builtin_expect?



3
ฉันคิดว่ารหัสโดยตรงของคุณควรเป็นif ( x == 0) {} else foo();.. หรือเพียงif ( x != 0 ) foo();ซึ่งเทียบเท่ากับรหัสจากเอกสารของ GCC
นาวาซ

คำตอบ:


187

ลองนึกภาพรหัสประกอบที่จะถูกสร้างขึ้นจาก:

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

ฉันคิดว่ามันควรจะเป็นสิ่งที่ชอบ:

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

คุณจะเห็นว่ามีการจัดเรียงคำสั่งตามลำดับที่barเคสนำหน้าfooเคส (ตรงข้ามกับรหัส C) สิ่งนี้สามารถใช้ประโยชน์ CPU ไปป์ไลน์ได้ดีขึ้นเนื่องจากการกระโดดข้ามคำแนะนำที่ดึงมาแล้ว

ก่อนที่จะดำเนินการกระโดดคำแนะนำด้านล่าง ( barกรณี) จะถูกส่งไปยังไปป์ไลน์ เนื่องจากfooกรณีไม่น่าเป็นไปได้การกระโดดก็ไม่น่าเป็นไปได้ดังนั้นการตีท่อจึงไม่น่าเป็นไปได้


1
มันใช้งานได้จริงเหรอ? ทำไมคำจำกัดความของ foo ไม่สามารถมาก่อนได้? ลำดับของนิยามฟังก์ชันนั้นไม่เกี่ยวข้องเท่าที่คุณมีต้นแบบใช่มั้ย
kingsmasher1

63
สิ่งนี้ไม่เกี่ยวกับนิยามฟังก์ชัน เป็นเรื่องเกี่ยวกับการจัดเรียงรหัสเครื่องใหม่ในลักษณะที่ทำให้มีโอกาสน้อยกว่าสำหรับ CPU ที่จะดึงข้อมูลคำสั่งที่จะไม่ถูกเรียกใช้งาน
Blagovest Buyukliev

4
โอ้เข้าใจแล้ว ดังนั้นคุณหมายถึงเนื่องจากมีความน่าจะเป็นสูงx = 0ดังนั้นจึงได้รับแท่งอันดับแรก และ foo นั้นถูกกำหนดในภายหลังเนื่องจากมีโอกาส (ค่อนข้างใช้ความน่าจะเป็น) น้อยใช่ไหม?
kingsmasher1

1
Ahhh..thanks นั่นเป็นคำอธิบายที่ดีที่สุด รหัสการชุมนุมทำจริงๆเคล็ดลับ :)
kingsmasher1

5
สิ่งนี้อาจฝังคำแนะนำสำหรับตัวทำนายสาขาของ CPU ปรับปรุงการวางท่อ
Hasturkun

50

ลองถอดรหัสเพื่อดูว่า GCC 4.8 ทำอะไรกับมัน

Blagovest พูดถึงการผกผันของสาขาเพื่อปรับปรุงไปป์ไลน์ แต่คอมไพเลอร์ปัจจุบันทำจริงๆหรือไม่? มาหาคำตอบกัน!

ไม่มี __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;
}

รวบรวมและถอดรหัสกับ GCC 4.8.2 x86_64 Linux:

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

เอาท์พุท:

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

ลำดับการเรียนการสอนในหน่วยความจำไม่เปลี่ยนแปลง: ก่อนputsแล้วจึงretqกลับมา

กับ __builtin_expect

ตอนนี้แทนที่if (i)ด้วย:

if (__builtin_expect(i, 0))

และเราได้รับ:

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ถูกย้ายไปยังส่วนท้ายสุดของฟังก์ชั่นที่retqกลับมา!

รหัสใหม่นั้นจะเหมือนกับ:

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

-O0การเพิ่มประสิทธิภาพนี้ไม่ได้ทำด้วย

แต่ขอให้โชคดีในการเขียนตัวอย่างที่ทำงานได้เร็ว__builtin_expectกว่าโดยไม่ต้องใช้ CPU ในวันนั้น ความพยายามที่ไร้เดียงสาของฉันอยู่ที่นี่

C ++ 20 [[likely]]และ[[unlikely]]

C ++ 20 ได้สร้างมาตรฐานในตัว C ++ เหล่านั้น: วิธีการใช้คุณลักษณะที่เป็นไปได้ / ไม่น่าเป็นไปได้ของ C ++ 20 ในคำสั่ง if-elseพวกเขามีแนวโน้มที่จะ (เล่นสำนวน!) ทำสิ่งเดียวกัน


1
ลองใช้ฟังก์ชั่น dispatch_once ของ libdispatch ซึ่งใช้ __builtin_expect เพื่อการปรับให้เหมาะสมที่สุด เส้นทางที่ช้านั้นทำงานเพียงครั้งเดียวและใช้ประโยชน์จาก __builtin_expect เพื่อบอกใบ้ถึงตัวทำนายสาขาว่าควรใช้เส้นทางที่รวดเร็ว เส้นทางที่รวดเร็วทำงานได้โดยไม่ต้องล็อคกุญแจเลย! mikeash.com/pyblog/…
Adam Kaplan

ดูเหมือนจะไม่สร้างความแตกต่างใน GCC 9.2: gcc.godbolt.org/z/GzP6cx (อันที่จริงแล้วใน 8.1)
Ruslan

40

แนวคิดของ__builtin_expectการบอกคอมไพเลอร์ว่าโดยปกติคุณจะพบว่านิพจน์ประเมินเป็น c เพื่อให้คอมไพเลอร์สามารถปรับให้เหมาะสมสำหรับกรณีนั้น

ฉันเดาว่ามีคนคิดว่าพวกเขาฉลาดและพวกเขาเร่งทำสิ่งนี้

น่าเสียดายที่หากสถานการณ์ไม่เป็นที่เข้าใจเป็นอย่างดี (เป็นไปได้ว่าพวกเขาไม่ได้ทำสิ่งนั้น) มันอาจทำให้สิ่งต่าง ๆ แย่ลง เอกสารยังพูดว่า:

โดยทั่วไปคุณควรใช้ความคิดเห็นโปรไฟล์จริงสำหรับสิ่งนี้ ( -fprofile-arcs) เนื่องจากโปรแกรมเมอร์ไม่ดีพอที่จะทำนายว่าโปรแกรมของพวกเขาทำงานได้จริงอย่างไร อย่างไรก็ตามมีแอพพลิเคชั่นที่รวบรวมข้อมูลนี้ยาก

โดยทั่วไปคุณไม่ควรใช้__builtin_expectยกเว้น:

  • คุณมีปัญหาเรื่องประสิทธิภาพที่แท้จริง
  • คุณได้เพิ่มประสิทธิภาพอัลกอริทึมในระบบอย่างเหมาะสมแล้ว
  • คุณได้รับข้อมูลประสิทธิภาพเพื่อสำรองการยืนยันของคุณว่ากรณีใดเป็นไปได้มากที่สุด

7
@Michael: นั่นไม่ใช่คำอธิบายเกี่ยวกับการคาดคะเนสาขา
Oliver Charlesworth

3
"โปรแกรมเมอร์ส่วนใหญ่ไม่ดี" หรือไม่ดีไปกว่าคอมไพเลอร์ คนงี่เง่าคนใดสามารถบอกได้ว่าในการวนรอบเงื่อนไขความต่อเนื่องน่าจะเป็นจริง แต่คอมไพเลอร์ก็รู้เช่นกันดังนั้นจึงไม่มีประโยชน์ที่จะบอก หากมีเหตุผลบางอย่างที่คุณเขียนห่วงว่าเกือบมักจะทำลายทันทีและถ้าคุณไม่สามารถให้ข้อมูลรายละเอียดที่จะคอมไพเลอร์สำหรับ PGO ที่แล้วอาจจะเขียนโปรแกรมคอมไพเลอร์รู้อะไรบางอย่างไม่ได้
Steve Jessop

15
ในบางสถานการณ์มันไม่สำคัญว่าสาขาใดมีแนวโน้มมากกว่า แต่เป็นสาขาที่สำคัญ หากสาขาที่ไม่คาดคิดนำไปสู่การยกเลิก () ความน่าจะเป็นไม่สำคัญและสาขาที่คาดหวังควรได้รับการจัดลำดับความสำคัญตามประสิทธิภาพเมื่อปรับให้เหมาะสม
Neowizard

1
ปัญหาเกี่ยวกับการอ้างสิทธิ์ของคุณคือการเพิ่มประสิทธิภาพของ CPU ที่สามารถทำได้เมื่อเทียบกับความน่าจะเป็นของสาขานั้น จำกัด อยู่ที่หนึ่ง: การคาดการณ์ของสาขาและการเพิ่มประสิทธิภาพนี้จะเกิดขึ้นไม่ว่าคุณจะใช้__builtin_expectหรือไม่ก็ตาม ในทางกลับกันคอมไพเลอร์สามารถทำการปรับให้เหมาะสมจำนวนมากตามความน่าจะเป็นของสาขาเช่นการจัดระเบียบโค้ดเพื่อให้พา ธ ร้อนติดกันโค้ดเคลื่อนที่ไม่น่าจะถูกปรับให้ไกลออกไปหรือลดขนาดลง ดีกว่าการจัดตารางเส้นทางร้อนและอื่น ๆ
BeeOnRope

1
... หากไม่มีข้อมูลจากผู้พัฒนามันจะตาบอดและเลือกกลยุทธ์ที่เป็นกลาง หากนักพัฒนาถูกต้องเกี่ยวกับความน่าจะเป็น (และในหลาย ๆ กรณีมันเป็นเรื่องไม่สำคัญที่จะเข้าใจว่าสาขามักจะนำ / ไม่ได้ถ่าย) - คุณได้รับผลประโยชน์เหล่านี้ หากคุณไม่ได้รับโทษ แต่ก็ไม่ได้ใหญ่ไปกว่าผลประโยชน์และเป็นช่วงเวลาที่สำคัญที่สุดไม่มีสิ่งใดที่จะมาแทนที่การคาดการณ์ของซีพียูได้
BeeOnRope

13

ตามที่ระบุไว้ในคำอธิบายเวอร์ชันแรกจะเพิ่มองค์ประกอบการคาดการณ์ให้กับการก่อสร้างโดยบอกคอมไพเลอร์ว่าx == 0สาขามีแนวโน้มมากขึ้นนั่นคือมันเป็นสาขาที่โปรแกรมของคุณจะใช้บ่อยขึ้น

เมื่อคำนึงถึงสิ่งนั้นคอมไพเลอร์สามารถปรับเงื่อนไขให้เหมาะสมเพื่อที่จะต้องใช้จำนวนงานน้อยที่สุดเมื่อเงื่อนไขที่คาดไว้มีค่าใช้จ่ายในการทำงานมากขึ้นในกรณีที่มีเงื่อนไขที่ไม่คาดคิด

ลองดูที่การใช้งานเงื่อนไขในระหว่างขั้นตอนการรวบรวมและในแอสเซมบลีที่เป็นผลลัพธ์เพื่อดูว่าสาขาหนึ่งอาจทำงานน้อยกว่าอีกสาขาหนึ่งได้อย่างไร

อย่างไรก็ตามฉันคาดหวังว่าการเพิ่มประสิทธิภาพนี้จะมีผลที่เห็นได้ชัดเจนหากเงื่อนไขที่เป็นปัญหาเป็นส่วนหนึ่งของวงในที่แน่นซึ่งเรียกว่ามากเนื่องจากความแตกต่างในโค้ดผลลัพธ์นั้นค่อนข้างเล็ก และหากคุณปรับให้เหมาะสมในทางที่ผิดคุณอาจลดประสิทธิภาพลงได้


แต่ท้ายที่สุดมันคือทั้งหมดที่เกี่ยวกับการตรวจสอบสภาพโดยคอมไพเลอร์คุณหมายความว่าคอมไพเลอร์จะถือว่าสาขาและเงินนี้เสมอและหลังจากนั้นถ้าไม่มีการแข่งขัน? เกิดอะไรขึ้น? ฉันคิดว่ามีบางสิ่งเพิ่มเติมเกี่ยวกับการทำนายสาขาในการออกแบบคอมไพเลอร์และวิธีการทำงาน
kingsmasher1

2
นี่คือการเพิ่มประสิทธิภาพขนาดเล็กอย่างแท้จริง เงยหน้าขึ้นมองว่ามีการใช้งานเงื่อนไขอย่างไรมีอคติเล็ก ๆ ต่อหนึ่งสาขา เป็นตัวอย่างสมมุติว่าเงื่อนไขจะกลายเป็นการทดสอบบวกกระโดดในการชุมนุม จากนั้นกิ่งกระโดดจะช้ากว่ากิ่งที่ไม่กระโดดดังนั้นคุณต้องการทำให้สาขาที่คาดหวังเป็นสาขาที่ไม่กระโดด
Kerrek SB

ขอบคุณไมเคิลและฉันคิดว่ามีมุมมองที่คล้ายกัน แต่ใส่ในคำที่แตกต่างกัน :-) ฉันเข้าใจภายในเรียบเรียงรวบรวมเกี่ยวกับการทดสอบและสาขาที่เป็นไปไม่ได้ที่จะอธิบายที่นี่ :)
kingsmasher1

นอกจากนี้ยังง่ายต่อการเรียนรู้ด้วยการค้นหาอินเทอร์เน็ต :-)
Kerrek SB

ฉันควรกลับไปที่หนังสือวิทยาลัยของฉันcompiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

ฉันไม่เห็นคำตอบใด ๆ ที่ตอบคำถามที่ฉันคิดว่าคุณกำลังถามถอดความ:

มีวิธีพกพามากขึ้นในการบอกกล่าวการคาดคะเนสาขากับคอมไพเลอร์หรือไม่

ชื่อคำถามของคุณทำให้ฉันคิดว่าจะทำอย่างนี้:

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

ถ้าคอมไพเลอร์สันนิษฐานว่า 'ความจริง' foo()มีโอกาสมากขึ้นก็อาจเพิ่มประสิทธิภาพจะไม่โทรหา

ปัญหาที่เกิดขึ้นที่นี่คือโดยทั่วไปคุณไม่ทราบว่าคอมไพเลอร์จะสันนิษฐานอะไร - ดังนั้นรหัสใด ๆ ที่ใช้เทคนิคประเภทนี้จะต้องมีการวัดอย่างระมัดระวัง (และอาจได้รับการตรวจสอบเมื่อเวลาผ่านไป


ในความเป็นจริงสิ่งนี้อาจเป็นสิ่งที่ OP ตั้งใจจะพิมพ์ (ตามที่ระบุในชื่อ) - แต่ด้วยเหตุผลบางอย่างการใช้งานelseถูกทิ้งไว้นอกร่างกายของโพสต์
Brent Bradburn

1

ฉันทดสอบบน Mac ตาม @Blagovest Buyukliev และ @Ciro แอสเซมบลีดูชัดเจนและฉันเพิ่มข้อคิดเห็น

คำสั่งคือ gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

เมื่อฉันใช้ -O3, มันดูเหมือนกันไม่ว่า __builtin_expect (i, 0) จะมีอยู่หรือไม่

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

เมื่อคอมไพล์ด้วย -O2, มันดูต่างกับและไม่มี __builtin_expect (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    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

ขณะนี้มี __builtin_expect (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

เพื่อสรุป __builtin_expect จะทำงานในกรณีสุดท้าย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.