यदि अन्य विवरणों में GCC के __builtin_expect का क्या लाभ है?


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();जो जीसीसी प्रलेखन से कोड के बराबर है।
नवाज

जवाबों:


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(सी कोड के विपरीत)। यह सीपीयू पाइपलाइन का बेहतर उपयोग कर सकता है, क्योंकि एक जंप पहले से ही प्राप्त निर्देशों को जोर देता है।

छलांग लगाने से पहले, इसके नीचे दिए गए निर्देशों ( barकेस) को पाइप लाइन में धकेल दिया जाता है। चूंकि fooमामला संभव नहीं है, कूदने की भी संभावना नहीं है, इसलिए पाइपलाइन को फेंकना संभव नहीं है।


1
क्या यह वास्तव में ऐसा काम करता है? फू परिभाषा पहले क्यों नहीं आ सकती है? फ़ंक्शन परिभाषाओं का क्रम अप्रासंगिक है, जहां तक ​​आपके पास एक प्रोटोटाइप है, है ना?
kingsmasher1

63
यह फ़ंक्शन परिभाषाओं के बारे में नहीं है। यह मशीन कोड को इस तरह से पुनर्व्यवस्थित करने के बारे में है जो सीपीयू के निर्देशों को लाने के लिए एक छोटी सी संभावना का कारण बनता है जिसे निष्पादित नहीं किया जा रहा है।
ब्लागॉवेस्ट ब्यूकलाइव सिप

4
ओह्ह मैं समझ गया तो आप का मतलब है क्योंकि एक उच्च संभावना है x = 0इसलिए बार पहले दिया गया है। और फू, बाद में परिभाषित किया गया है क्योंकि यह संभावना है (बल्कि संभावना का उपयोग करें) कम है, है ना?
kingsmasher1

1
Ahhh..thanks। यही सबसे अच्छा स्पष्टीकरण है। असेंबली कोड ने वास्तव में चाल बनाई :)
kingsmasher1

5
यह सीपीयू शाखा भविष्यवक्ता के लिए संकेत भी एम्बेड कर सकता है , पाइपलाइनिंग में सुधार कर सकता है
हस्त्कुर्क

50

जीसीसी 4.8 इसके साथ क्या करता है यह देखने के लिए विघटित करते हैं

ब्लागॉवेस्ट ने पाइपलाइन को बेहतर बनाने के लिए शाखा व्युत्क्रम का उल्लेख किया, लेकिन क्या वर्तमान संकलक वास्तव में ऐसा करते हैं? चलो पता करते हैं!

के बिना __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;
}

संकलन और जीसीसी 4.8.2 x86_64 लिनक्स के साथ विघटित:

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बिना किसी उदाहरण के साथ तेजी से चलने वाले उदाहरण को लिखने का सौभाग्य , उन दिनों सीपीयू वास्तव में स्मार्ट हैं । मेरी भोली कोशिश यहाँ हैं

सी ++ 20 [[likely]]और[[unlikely]]

C ++ 20 ने उन C ++ बिल्ट-इन को मानकीकृत किया है: C ++ 20 की संभावना / संभावना नहीं का उपयोग करने के लिए if-else स्टेटमेंट में वे संभवतया (एक वाक्य!) एक ही काम करेंगे।


1
एक व्यावहारिक अनुकूलन के लिए __builtin_expect का उपयोग करने वाले libdispatch के डिस्पैच_ऑनस फ़ंक्शन को देखें। धीमा रास्ता एक-समय पर चलता है और शाखा के भविष्यवक्ता को संकेत देने के लिए __builtin_expect का उपयोग करता है कि तेज़ पथ लिया जाना चाहिए। तेज रास्ता बिना किसी ताले का उपयोग किए चलता है! mikeash.com/pyblog/…
एडम कपलान

GCC 9.2 में कोई अंतर नहीं दिखता है: gcc.godbolt.org/z/GzP6cx (वास्तव में, पहले से 8.1 में)
रुस्लान

40

__builtin_expectसंकलक को यह बताने का विचार है कि आप आमतौर पर पाएंगे कि अभिव्यक्ति ग का मूल्यांकन करती है, ताकि संकलक उस मामले के लिए अनुकूलन कर सके।

मुझे लगता है कि किसी ने सोचा था कि वे चतुर थे और वे ऐसा करके चीजों को गति दे रहे थे।

दुर्भाग्य से, जब तक कि स्थिति बहुत अच्छी तरह से समझ में नहीं आती है (यह संभावना है कि उन्होंने ऐसा कुछ नहीं किया है), यह अच्छी तरह से चीजों को खराब कर सकता है। प्रलेखन भी कहता है:

सामान्य तौर पर, आपको इसके लिए वास्तविक प्रोफ़ाइल फ़ीडबैक का उपयोग करना चाहिए ( -fprofile-arcs), क्योंकि प्रोग्रामर यह अनुमान लगाने में बेहद खराब हैं कि उनके कार्यक्रम वास्तव में कैसा प्रदर्शन करते हैं। हालांकि, ऐसे अनुप्रयोग हैं जिनमें यह डेटा एकत्र करना कठिन है।

सामान्य तौर पर, आपको __builtin_expectतब तक उपयोग नहीं करना चाहिए जब तक कि:

  • आपके पास एक बहुत अच्छा प्रदर्शन मुद्दा है
  • आपने पहले ही सिस्टम में एल्गोरिदम को उचित रूप से अनुकूलित कर लिया है
  • आपको अपने दावे का समर्थन करने के लिए प्रदर्शन डेटा मिला है कि किसी विशेष मामले की सबसे अधिक संभावना है

7
@ मिचेल: यह वास्तव में शाखा की भविष्यवाणी का वर्णन नहीं है।
ओलिवर चार्ल्सवर्थ

3
"अधिकांश प्रोग्रामर BAD हैं" या वैसे भी संकलक से बेहतर नहीं है। कोई भी बेवकूफ बता सकता है कि लूप के लिए, निरंतरता की स्थिति सही होने की संभावना है, लेकिन कंपाइलर जानता है कि यह बताने का कोई लाभ नहीं है। यदि किसी कारण से आपने एक लूप लिखा है जो लगभग हमेशा तुरंत टूट जाएगा, और यदि आप पीजीओ के लिए संकलक को प्रोफ़ाइल डेटा प्रदान नहीं कर सकते हैं, तो शायद प्रोग्रामर को पता है कि कंपाइलर कुछ नहीं करता है।
स्टीव जेसप

15
कुछ स्थितियों में, यह मायने नहीं रखता कि कौन सी शाखा अधिक संभावित है, बल्कि यह कि कौन सी शाखा मायने रखती है। यदि अनपेक्षित शाखा गर्भपात () की ओर ले जाती है, तो संभावना कोई मायने नहीं रखती है, और अनुकूलन करते समय अपेक्षित शाखा को प्रदर्शन-प्राथमिकता दी जानी चाहिए।
नियोवॉगर

1
आपके दावे के साथ समस्या यह है कि सीपीयू शाखा संभाव्यता के संबंध में जो अनुकूलन कर सकता है, वह एक से बहुत अधिक सीमित है: शाखा भविष्यवाणी, और यह अनुकूलन होता है कि आप उपयोग करते हैं __builtin_expectया नहीं । दूसरी ओर, संकलक शाखा संभाव्यता के आधार पर कई अनुकूलन कर सकता है, जैसे कि कोड को व्यवस्थित करना ताकि गर्म मार्ग सन्निहित है, चलती कोड को आगे दूर अनुकूलित करने या इसके आकार को कम करने की संभावना नहीं है, इस बारे में निर्णय लेते हुए कि कौन सी शाखाओं को वेक्टर करना है, बेहतर गर्म रास्ता तय करना, और इसी तरह।
मधुमक्खी पालन

1
... डेवलपर की जानकारी के बिना, यह अंधा है और एक तटस्थ रणनीति चुनता है। यदि डेवलपर संभावनाओं के बारे में सही है (और कई मामलों में यह समझना तुच्छ है कि एक शाखा आमतौर पर ली जाती है / नहीं ली जाती है) - आपको ये लाभ मिलते हैं। यदि आपको कुछ दंड नहीं मिला है, लेकिन यह किसी भी तरह से लाभ से बहुत बड़ा नहीं है, और सबसे गंभीर रूप से, इस में से कोई भी किसी भी तरह से सीपीयू शाखा की भविष्यवाणी को ओवरराइड करता है।
मधुमक्खी पालन

13

खैर, जैसा कि यह वर्णन में कहता है, पहला संस्करण निर्माण के लिए एक भविष्य कहनेवाला तत्व जोड़ता है, संकलक को बता रहा है कि x == 0शाखा अधिक संभावना है - अर्थात, यह वह शाखा है जिसे आपके कार्यक्रम द्वारा अधिक बार लिया जाएगा।

इसे ध्यान में रखते हुए, कंपाइलर सशर्त को अनुकूलित कर सकता है ताकि अपेक्षित स्थिति होने पर कम से कम काम की आवश्यकता हो, अप्रत्याशित स्थिति के मामले में अधिक काम करने की हो सकता है।

संकलन चरण के दौरान सशर्त कैसे लागू किया जाता है, और परिणामी विधानसभा में भी, यह देखने के लिए कि एक शाखा दूसरे की तुलना में कम कैसे हो सकती है।

हालाँकि, मैं केवल इस अनुकूलन को ध्यान देने योग्य प्रभाव की उम्मीद करूंगा यदि प्रश्न में सशर्त एक तंग आंतरिक लूप का हिस्सा है जिसे बहुत कहा जाता है , क्योंकि परिणामी कोड में अंतर अपेक्षाकृत छोटा है। और यदि आप इसे गलत तरीके से गोल करते हैं, तो आप अपने प्रदर्शन को कम कर सकते हैं।


लेकिन अंत में यह सब संकलक द्वारा स्थिति की जांच करने के बारे में है, क्या आपके कहने का मतलब है कि संकलक हमेशा इस शाखा को मानता है और आगे बढ़ता है, और बाद में अगर कोई मेल नहीं है? क्या होता है? मुझे लगता है कि संकलक डिजाइन में इस शाखा की भविष्यवाणी के सामान के बारे में कुछ और है, और यह कैसे काम करता है।
kingsmasher1

2
यह वास्तव में एक माइक्रो-अनुकूलन है। देखो कैसे सशर्त लागू किया जाता है, एक शाखा के प्रति एक छोटा सा पूर्वाग्रह है। एक काल्पनिक उदाहरण के रूप में, मान लीजिए कि एक सशर्त परीक्षण के लिए विधानसभा में एक छलांग हो जाती है। तब जंपिंग शाखा नॉन-जंपिंग की तुलना में धीमी होती है, इसलिए आप अपेक्षित शाखा को नॉन-जंपिंग बनाना पसंद करेंगे।
केरेक एसबी

धन्यवाद, आपके और माइकल को लगता है कि मेरे विचार समान हैं लेकिन अलग-अलग शब्दों में दिए गए हैं :-) मुझे समझ में आया कि टेस्ट-और-ब्रांच के बारे में सटीक कंपाइलर के बारे में यहाँ बता
पाना

वे इंटरनेट पर खोज करके भी बहुत आसानी से सीख लेते हैं :-)
Kerrek SB

मैं बेहतर तरीके से अपने कॉलेज की किताब compiler design - Aho, Ullmann, Sethi:-)
kingsmasher1

1

मुझे इस सवाल का कोई जवाब नहीं दिखाई दे रहा है कि मुझे लगता है कि आप पूछ रहे थे कि क्या है?

संकलक के लिए शाखा भविष्यवाणी को इंगित करने का एक और अधिक पोर्टेबल तरीका है।

आपके प्रश्न के शीर्षक ने मुझे इस तरह से करने के बारे में सोचा:

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

यदि संकलक मानता है कि 'सत्य' अधिक संभावना है, तो यह कॉल न करने के लिए अनुकूलन कर सकता है foo()

यहाँ समस्या यह है कि आप सामान्य रूप से नहीं जानते हैं कि संकलक क्या मानेंगे - इसलिए इस तरह की तकनीक का उपयोग करने वाले किसी भी कोड को सावधानीपूर्वक मापा जाना चाहिए (और संभवत: यदि संदर्भ बदलता है तो समय के साथ निगरानी की जाए)।


यह वास्तव में हो सकता है, ओपी ने मूल रूप से टाइप करने का इरादा किया था (जैसा कि शीर्षक से संकेत मिलता है) - लेकिन किसी कारण elseसे पोस्ट के शरीर से बाहर छोड़ दिया गया था।
ब्रेंट बर्नबर्न

1

मैं इसे मैक पर @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 अंतिम मामले में काम करता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.