लिनक्स कर्नेल में काम करने की संभावना / संभावना नहीं है और उनके लाभ क्या हैं?


349

मैं लिनक्स कर्नेल के कुछ हिस्सों के माध्यम से खुदाई कर रहा हूं, और इस तरह की कॉल मिली:

if (unlikely(fd < 0))
{
    /* Do something */
}

या

if (likely(!err))
{
    /* Do something */
}

मुझे उनकी परिभाषा मिली है:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

मुझे पता है कि वे अनुकूलन के लिए हैं, लेकिन वे कैसे काम करते हैं? और उनके उपयोग से कितना प्रदर्शन / आकार में कमी की उम्मीद की जा सकती है? और क्या यह परेशानी के लायक है (और पोर्टेबिलिटी को खोना) कम से कम अड़चन कोड (यूजरस्पेस में, निश्चित रूप से)।


7
यह वास्तव में लिनक्स कर्नेल या मैक्रोज़ के बारे में विशिष्ट नहीं है, लेकिन एक संकलक अनुकूलन है। क्या इसे दर्शाने के लिए इसका प्रतिकार किया जाना चाहिए?
कोड़ी ब्रोचेस


2
यह भी देखेंBOOST_LIKELY
रग्गरो तुर्रा

4
संबंधित: एक बेंचमार्क एक__builtin_expect और प्रश्न के उपयोग पर।
YSC

13
कोई पोर्टेबिलिटी समस्या नहीं है। आप इस तरह की हिंटिंग का समर्थन नहीं करने वाले प्लेटफ़ॉर्म की तरह #define likely(x) (x)और चीजों पर तुच्छता से कर सकते हैं #define unlikely(x) (x)
डेविड श्वार्ट्ज

जवाबों:


329

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

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


3
ये मैक्रोज़ ज्यादातर त्रुटि जाँच के लिए उपयोग किए जाते थे। क्योंकि त्रुटि शायद कम हो जाती है तो सामान्य ऑपरेशन। कुछ लोग प्रोफाइलिंग या कैलकुलेशन को सबसे ज्यादा इस्तेमाल किए जाने वाले पत्ते को तय करने के लिए करते हैं ...
gavenkoa

51
खंड के संबंध में "[...]that it is being run in a tight loop", कई सीपीयू में एक शाखा पूर्वसूचक होता है , इस प्रकार इन मैक्रोज़ का उपयोग करने से केवल पहली बार कोड को निष्पादित करने में मदद मिलती है या जब इतिहास तालिका को एक ही सूचकांक के साथ शाखा शाखा में एक अलग शाखा द्वारा अधिलेखित किया जाता है। एक तंग लूप में, और एक शाखा को संभालने का ज्यादातर समय एक ही रास्ता जाता है, शाखा भविष्यवक्ता संभवतः बहुत जल्दी सही शाखा का अनुमान लगाना शुरू कर देगा। - पैदल यात्रा में आपका दोस्त।
रॉस रोजर्स

8
@RossRogers: वास्तव में क्या होता है संकलक शाखाओं की व्यवस्था करता है इसलिए सामान्य मामला एक नहीं लिया जाता है। यह तब और भी तेज होता है जब शाखा की भविष्यवाणी काम करती है। जब वे पूरी तरह से भविष्यवाणी की जाती हैं तब भी निर्देश-प्राप्त करने और डिकोड करने के लिए ली गई शाखाएँ समस्याग्रस्त होती हैं। कुछ सीपीयू उन शाखाओं की भविष्यवाणी करते हैं जो आमतौर पर उनकी इतिहास तालिका में नहीं होती हैं, आमतौर पर आगे की शाखाओं के लिए नहीं ली गई हैं। इंटेल सीपीयू उस तरह से काम नहीं करते हैं: वे यह जांचने की कोशिश नहीं करते हैं कि इस शाखा के लिए पूर्वसूचक तालिका प्रविष्टि है , वे इसे वैसे भी उपयोग करते हैं। एक गर्म शाखा और एक ठंडी शाखा एक ही प्रविष्टि का नाम दे सकती है ...
पीटर कॉर्ड्स

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

3
@BeeOnRope कैश प्रीफ़ैच और शब्द आकार के कारण, अभी भी प्रोग्राम को रैखिक रूप से चलाने का एक फायदा है। अगला मेमोरी लोकेशन पहले ही प्राप्त हो जाएगा और कैश में, शाखा लक्ष्य हो सकता है या नहीं। 64 बिट सीपीयू के साथ आप एक बार में कम से कम 64 बिट्स हड़पते हैं। DRAM इंटरलेव के आधार पर, यह 2x 3x या अधिक बिट्स हो सकते हैं जो कि हड़प जाते हैं।
ब्राइस

88

जीसीसी 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)
        printf("%d\n", 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 14                   jne    24 <main+0x24>
  10:       ba 01 00 00 00          mov    $0x1,%edx
  15:       be 00 00 00 00          mov    $0x0,%esi
                    16: R_X86_64_32 .rodata.str1.1
  1a:       bf 01 00 00 00          mov    $0x1,%edi
  1f:       e8 00 00 00 00          callq  24 <main+0x24>
                    20: R_X86_64_PC32       __printf_chk-0x4
  24:       bf 00 00 00 00          mov    $0x0,%edi
                    25: R_X86_64_32 .rodata.str1.1+0x4
  29:       e8 00 00 00 00          callq  2e <main+0x2e>
                    2a: R_X86_64_PC32       puts-0x4
  2e:       31 c0                   xor    %eax,%eax
  30:       48 83 c4 08             add    $0x8,%rsp
  34:       c3                      retq

स्मृति में निर्देश क्रम अपरिवर्तित था: पहले printfऔर फिर 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 11                   je     21 <main+0x21>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1+0x4
  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
  21:       ba 01 00 00 00          mov    $0x1,%edx
  26:       be 00 00 00 00          mov    $0x0,%esi
                    27: R_X86_64_32 .rodata.str1.1
  2b:       bf 01 00 00 00          mov    $0x1,%edi
  30:       e8 00 00 00 00          callq  35 <main+0x35>
                    31: R_X86_64_PC32       __printf_chk-0x4
  35:       eb d9                   jmp    10 <main+0x10>

printf(संकलित __printf_chk), समारोह के अंत करने के लिए ले जाया गया था के बाद putsके रूप में अन्य उत्तर ने उल्लेख किया और बदले शाखा भविष्यवाणी में सुधार होगा।

तो यह मूल रूप से एक ही है:

int main() {
    int i = !time(NULL);
    if (i)
        goto printf;
puts:
    puts("a");
    return 0;
printf:
    printf("%d\n", i);
    goto puts;
}

यह अनुकूलन के साथ नहीं किया गया था -O0

लेकिन __builtin_expectबिना किसी उदाहरण के तेजी से चलने वाला उदाहरण लिखने का सौभाग्य , इन दिनों सीपीयू वास्तव में स्मार्ट हैं । मेरी भोली कोशिश यहाँ हैं

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

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


71

ये मैक्रोज़ हैं जो कंपाइलर को संकेत देते हैं कि किस रास्ते से शाखा जा सकती है। यदि वे उपलब्ध हैं, तो मैक्रोज़ जीसीसी विशिष्ट एक्सटेंशन तक विस्तारित होते हैं।

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

if (unlikely(x)) {
  dosomething();
}

return x;

फिर यह इस कोड को कुछ और जैसा बनाने के लिए पुनर्गठन कर सकता है:

if (!x) {
  return x;
}

dosomething();
return x;

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

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

इन परिदृश्यों में कंपाइलर और प्रोसेसर का उपयोग करने वाली कई अन्य रणनीतियाँ हैं। आप अधिक जानकारी प्राप्त कर सकते हैं कि विकिपीडिया पर शाखा के भविष्यवक्ता कैसे काम करते हैं: http://en.wikipedia.org/wiki/Branch_nededor


3
इसके अलावा, यह icache पदचिह्न को प्रभावित करता है - गर्म रास्ते से कोड के स्निपेट की संभावना नहीं रखने से।
fche

2
अधिक सटीक रूप से, यह gotoबिना दोहराए इसके साथ कर सकता है return x: stackoverflow.com/a/31133787/895245
Ciro Santilli 郝海东 do do do

7

वे संकलक के कारण उपयुक्त शाखा संकेत का उत्सर्जन करते हैं जहां हार्डवेयर उनका समर्थन करता है। यह आमतौर पर सिर्फ निर्देश opcode में कुछ बिट्स twiddling का मतलब है, इसलिए कोड आकार नहीं बदलेगा। सीपीयू अनुमानित स्थान से निर्देश प्राप्त करना शुरू कर देगा, और पाइप लाइन को फ्लश करेगा और शुरू हो जाएगा यदि शाखा पहुंचते ही गलत हो जाता है; मामले में जहां संकेत सही है, यह शाखा को बहुत तेज कर देगा - ठीक है कि हार्डवेयर पर कितना तेजी से निर्भर करेगा; और यह कोड के प्रदर्शन को कितना प्रभावित करता है यह इस बात पर निर्भर करेगा कि समय संकेत किस अनुपात में सही है।

उदाहरण के लिए, PowerPC CPU पर एक अनइंस्टैंट ब्रांच 16 साइकिल ले सकती है, एक सही ढंग से एक 8 और एक गलत तरीके से एक 24 संकेत दिया गया है। अंतरतम छोरों में अच्छा संकेत एक बहुत बड़ा अंतर ला सकता है।

पोर्टेबिलिटी वास्तव में एक मुद्दा नहीं है - संभवतः परिभाषा एक प्रति-प्लेटफॉर्म हेडर में है; आप बस "संभावना" और "संभावनाहीन" को उन प्लेटफार्मों के लिए कुछ भी नहीं कह सकते हैं जो स्थिर शाखा संकेत का समर्थन नहीं करते हैं।


3
रिकॉर्ड के लिए, x86 शाखा संकेत के लिए अतिरिक्त स्थान लेता है। उपयुक्त संकेत निर्दिष्ट करने के लिए आपके पास शाखाओं पर एक-बाइट उपसर्ग होना चाहिए। सहमत हैं कि हिंटिंग एक अच्छी बात (टीएम) है, हालांकि।
कोड़ी ब्रोशर

2
डांग CISC सीपीयू और उनके चर-लंबाई निर्देश;)
चांदनी

3
डांग आरआईएससी सीपीयू - मेरे 15-बाइट के निर्देशों से दूर रहें;)
कोडी ब्रोएस

7
@ कोडीक्रोसियस: शाखा संकेत को P4 के साथ पेश किया गया था, लेकिन P4 के साथ छोड़ दिया गया था। अन्य सभी x86 सीपीयू केवल उन उपसर्गों को अनदेखा करते हैं (क्योंकि उपसर्ग हमेशा संदर्भों में अनदेखा किए जाते हैं जहां वे अर्थहीन होते हैं)। ये मैक्रो वास्तव में x86 पर शाखा-संकेत उपसर्गों का उत्सर्जन करने के लिए जीसीसी का कारण नहीं बनते हैं। वे फास्ट-पाथ पर कम ली गई शाखाओं के साथ अपने फ़ंक्शन को पूरा करने के लिए gcc प्राप्त करने में आपकी सहायता करते हैं।
पीटर कॉर्ड्स

5
long __builtin_expect(long EXP, long C);

यह निर्माण संकलक को बताता है कि अभिव्यक्ति EXP की संभावना सबसे अधिक मूल्य C होगी। वापसी मूल्य EXP है। __builtin_expect का अर्थ सशर्त अभिव्यक्ति में किया जाना है। लगभग सभी मामलों में इसका उपयोग बूलियन अभिव्यक्तियों के संदर्भ में किया जाएगा, जिस स्थिति में यह दो सहायक मैक्रो को परिभाषित करने के लिए अधिक सुविधाजनक है:

#define unlikely(expr) __builtin_expect(!!(expr), 0)
#define likely(expr) __builtin_expect(!!(expr), 1)

इन मैक्रोज़ को तब उपयोग में लाया जा सकता है

if (likely(a > 1))

संदर्भ: https://www.akkadia.org/drepper/cpumemory.pdf


1
जैसा कि एक अन्य जवाब में एक टिप्पणी में पूछा गया था - मैक्रोज़ में दोहरे उलटफेर का कारण क्या है (यानी __builtin_expect(!!(expr),0)सिर्फ इसके बजाय क्यों उपयोग करें __builtin_expect((expr),0)?
माइकल फर्थ

1
@MichaelFirth "डबल उलटा" !!कुछ करने के लिए कास्टिंग के बराबर है bool। कुछ लोग इसे इस तरह लिखना पसंद करते हैं।
बेन एक्सओ

2

(सामान्य टिप्पणी - अन्य उत्तर विवरण को कवर करते हैं)

कोई कारण नहीं है कि आपको उनका उपयोग करके पोर्टेबिलिटी को खोना चाहिए।

आपके पास हमेशा एक सरल नील-प्रभाव "इनलाइन" या मैक्रो बनाने का विकल्प होता है जो आपको अन्य संकलक के साथ अन्य प्लेटफार्मों पर संकलन करने की अनुमति देगा।

यदि आप अन्य प्लेटफार्मों पर हैं तो आपको केवल अनुकूलन का लाभ नहीं मिलेगा।


1
आप पोर्टेबिलिटी का उपयोग नहीं करते हैं - प्लेटफ़ॉर्म जो उनका समर्थन नहीं करते हैं उन्हें खाली तारों के विस्तार के लिए परिभाषित करें।
sharptooth

2
मुझे लगता है कि आप दोनों वास्तव में एक-दूसरे से सहमत हैं - यह सिर्फ भ्रमित करने वाला है। (इसके लुक से, एंड्रयू की टिप्पणी कह रही है कि "आप उन्हें पोर्टेबिलिटी खोए बिना उपयोग कर सकते हैं" लेकिन शार्पूथ ने सोचा कि उन्होंने कहा कि "उनका उपयोग वे पोर्टेबल नहीं हैं" और आपत्ति करें।)
मिरल

2

कोडी की टिप्पणी के अनुसार , इसका लिनक्स से कोई लेना-देना नहीं है, लेकिन कंपाइलर के लिए एक संकेत है। क्या होता है वास्तुकला और संकलक संस्करण पर निर्भर करेगा।

लिनक्स में यह विशेष सुविधा ड्राइवरों में कुछ गलत उपयोग की जाती है। के रूप में osgx में बाहर अंक गर्म विशेषता के शब्दों में, किसी भी hotया coldके साथ बुलाया समारोह एक ब्लॉक में स्वचालित रूप से संकेत कर सकते हैं कि हालत होने की संभावना है या नहीं। उदाहरण के लिए, dump_stack()चिह्नित किया गया है coldइसलिए यह निरर्थक है,

 if(unlikely(err)) {
     printk("Driver error found. %d\n", err);
     dump_stack();
 }

भविष्य के संस्करण gccइन संकेतों के आधार पर किसी फ़ंक्शन को चुनिंदा रूप से इनलाइन कर सकते हैं। ऐसे सुझाव भी आए हैं कि यह नहीं है boolean, लेकिन सबसे अधिक संभावना के रूप में एक स्कोर , आदि। आम तौर पर, इसे कुछ वैकल्पिक तंत्र का उपयोग करने के लिए पसंद किया जाना चाहिए cold। किसी भी जगह लेकिन गर्म रास्तों में इसका उपयोग करने का कोई कारण नहीं है। एक आर्किटेक्चर पर एक कंपाइलर क्या करेगा दूसरे पर पूरी तरह से अलग हो सकता है।


2

कई लिनक्स रिलीज़ में, आप complier.h / usr / linux / में पा सकते हैं, आप इसे बस उपयोग के लिए शामिल कर सकते हैं। और एक अन्य राय, संभावना नहीं है () संभावना के बजाय अधिक उपयोगी है (), क्योंकि

if ( likely( ... ) ) {
     doSomething();
}

यह कई संकलक में भी अनुकूलित किया जा सकता है।

और वैसे, यदि आप कोड के विस्तृत व्यवहार का निरीक्षण करना चाहते हैं, तो आप बस अनुसरण के रूप में कर सकते हैं:

gcc -c test.c objdump -d test.o> obj.s

फिर, obj.s खोलें, आप उत्तर पा सकते हैं।


1

वे संकलक को संकेत देते हैं कि वे शाखाओं पर संकेत उपसर्ग उत्पन्न करते हैं। X86 / x64 पर, वे एक बाइट लेते हैं, इसलिए आपको प्रत्येक शाखा के लिए अधिकतम एक बाइट में वृद्धि मिलेगी। प्रदर्शन के लिए, यह पूरी तरह से आवेदन पर निर्भर करता है - ज्यादातर मामलों में, प्रोसेसर पर शाखा भविष्यवक्ता इन दिनों उनकी उपेक्षा करेंगे।

संपादित करें: एक जगह के बारे में वे वास्तव में मदद कर सकते हैं भूल गए। यह संकलक को 'संभावना' पथ के लिए ली गई शाखाओं की संख्या को कम करने के लिए नियंत्रण-प्रवाह ग्राफ को फिर से व्यवस्थित करने की अनुमति दे सकता है। यह उन छोरों में एक चिह्नित सुधार हो सकता है जहां आप कई निकास मामलों की जांच कर रहे हैं।


10
जीसीसी कभी भी x86 शाखा संकेत उत्पन्न नहीं करता है - कम से कम सभी इंटेल सीपीयू उन्हें वैसे भी अनदेखा करेंगे। हालांकि, यह इनलाइनिंग और लूप के अनियंत्रित होने से बचने के लिए असंभावित क्षेत्रों में कोड आकार को सीमित करने का प्रयास करेगा।
एलेक्स विचित्र

1

ये प्रोग्रामर के लिए GCC फ़ंक्शंस हैं जो कंपाइलर को संकेत देते हैं कि किसी दिए गए एक्सप्रेशन में सबसे संभावित ब्रांच की स्थिति क्या होगी। यह संकलक को शाखा निर्देशों का निर्माण करने की अनुमति देता है ताकि सबसे आम मामला निष्पादित करने के लिए सबसे कम संख्या में निर्देश ले।

शाखा निर्देश कैसे बनाए जाते हैं यह प्रोसेसर आर्किटेक्चर पर निर्भर करता है।

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