2016-12-28 10 views
13

मैंने तुलना करने के लिए एक सरल प्रयोग किया, और अगर केवल एक (डिफ़ॉल्ट मान प्रीसेट के साथ)। उदाहरण:यदि डिफ़ॉल्ट है तो डिफ़ॉल्ट है?

void test0(char c, int *x) { 
    *x = 0; 
    if (c == 99) { 
     *x = 15; 
    } 
} 

void test1(char c, int *x) { 
    if (c == 99) { 
     *x = 15; 
    } else { 
     *x = 0; 
    } 
} 

ऊपर कार्यों के लिए, मैं (cmovne उपयोग करते हुए) ठीक उसी विधानसभा कोड मिला है।

void test2(char c, int *x, int *y) { 
    *x = 0; 
    *y = 0; 
    if (c == 99) { 
     *x = 15; 
     *y = 21; 
    } 
} 

void test3(char c, int *x, int *y) { 
    if (c == 99) { 
     *x = 15; 
     *y = 21; 
    } else { 
     *x = 0; 
     *y = 0; 
    } 
} 

विधानसभा अचानक अलग हो जाता है::

test2(char, int*, int*): 
     cmp  dil, 99 
     mov  DWORD PTR [rsi], 0 
     mov  DWORD PTR [rdx], 0 
     je  .L10 
     rep ret 
.L10: 
     mov  DWORD PTR [rsi], 15 
     mov  DWORD PTR [rdx], 21 
     ret 
test3(char, int*, int*): 
     cmp  dil, 99 
     je  .L14 
     mov  DWORD PTR [rsi], 0 
     mov  DWORD PTR [rdx], 0 
     ret 
.L14: 
     mov  DWORD PTR [rsi], 15 
     mov  DWORD PTR [rdx], 21 
     ret 

ऐसा लगता है फर्क सिर्फ इतना है अगर शीर्ष mov रों पहले या je के बाद किया जाता है है कि

लेकिन जब एक अतिरिक्त चर जोड़ने ।

अब (क्षमा करें मेरी असेंबली थोड़ा कच्ची है), क्या पाइपलाइन फ्लश को बचाने के लिए कूदने के बाद mov एस हमेशा बेहतर नहीं होता है? और यदि ऐसा है तो अनुकूलक (gcc6.2 -O3) बेहतर विधि का उपयोग क्यों नहीं करेगा?

+0

मुझे लगता है कि सवाल पर upvotes का एक बहुत की भविष्यवाणी :) –

+2

आपको आश्चर्य है कि क्या एक्स वाई की तुलना में तेजी है, तो आप प्रोफ़ाइल चाहिए, _profile_ और __profile__। केवल सटीक समय समस्या पर कुछ प्रकाश डालेगा। – ForceBru

+0

क्या आप पूछ रहे हैं कि कोई अन्य (प्रोफाइल) से तेज है या विधानसभा अलग क्यों है? – juanchopanza

उत्तर

14

ऊपर कार्यों के लिए, मैं ठीक उसी विधानसभा कोड (का उपयोग कर cmovne) मिला है।

निश्चित रूप से, कुछ कंपाइलर अनुकूलन कर सकते हैं, लेकिन इसकी गारंटी नहीं है। यह बहुत संभव है कि आपको फ़ंक्शन लिखने के उन दो तरीकों के लिए अलग ऑब्जेक्ट कोड मिलेगा।

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

test0 PROC 
    cmp  BYTE PTR [c], 99 
    mov  eax, DWORD PTR [x] 
    mov  DWORD PTR [eax], 0 
    jne  SHORT LN2 
    mov  DWORD PTR [eax], 15 
LN2: 
    ret  0 
test0 ENDP 
test1 PROC 
    mov  eax, DWORD PTR [x] 
    xor  ecx, ecx 
    cmp  BYTE PTR [c], 99 
    setne cl 
    dec  ecx 
    and  ecx, 15 
    mov  DWORD PTR [eax], ecx 
    ret  0 
test1 ENDP 

ध्यान दें कि test1 आप शाखा कोड कि SETNE अनुदेश (एक सशर्त का इस्तेमाल करता है देता है:

यहाँ MSVC के पुराने संस्करणों जब x86-32 को लक्षित उत्पन्न होगा (मुख्य रूप से because they don't know to use the CMOV instruction) है सेट करें, जो इस मामले में NE) को सही मूल्य उत्पन्न करने के लिए कुछ बिट-मैनिपुलेशन के संयोजन के साथ स्थिति कोड के आधार पर 0 या 1 को सेट करेगा। test0 15 से *x के असाइनमेंट को छोड़ने के लिए एक सशर्त शाखा का उपयोग करता है।

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

हालांकि अतिरिक्त चर जोड़ने पर ...असेंबली अचानक अलग हो जाती है

ठीक है, कोई आश्चर्य नहीं है। कोड में एक छोटा सा परिवर्तन अक्सर उत्सर्जित कोड पर महत्वपूर्ण प्रभाव डाल सकता है। अनुकूलक जादू नहीं हैं; वे वास्तव में जटिल पैटर्न matchers हैं। आपने पैटर्न बदल दिया!

अनुमोदित, एक अनुकूलन कंपाइलर ब्रांचलेस कोड उत्पन्न करने के लिए यहां दो सशर्त चालों का उपयोग कर सकता है। वास्तव में, यह ठीक है कि क्लैंग 3.9 test3 (लेकिन test2 के लिए नहीं है, हमारे उपर्युक्त विश्लेषण के अनुरूप है कि यह दिखाता है कि अनुकूलक असामान्य लोगों की तुलना में मानक पैटर्न को पहचानने में सक्षम हो सकते हैं)। लेकिन जीसीसी ऐसा नहीं करता है। फिर, एक विशेष अनुकूलन की कोई गारंटी नहीं है।

ऐसा लगता है कि एकमात्र अंतर यह है कि शीर्ष "mov" को "je" से पहले या बाद में किया जाता है।

अब (क्षमा करें मेरी असेंबली थोड़ा कच्ची है), क्या पाइपलाइन फ्लश को बचाने के लिए कूदने के बाद mov हमेशा बेहतर नहीं होता है?

नहीं, वास्तव में नहीं। इससे इस मामले में कोड में सुधार नहीं होगा। यदि शाखा का गलत अनुमान लगाया गया है, तो आप एक पाइपलाइन फ्लश होने जा रहे हैं इससे कोई फर्क नहीं पड़ता। इससे कोई फर्क नहीं पड़ता कि अनुमानित रूप से गलत अनुमानित कोड ret निर्देश है या यदि यह mov निर्देश है।

एकमात्र कारण यह होगा कि ret निर्देश तुरंत एक सशर्त शाखा का पालन करता है यदि आप हाथ से असेंबली कोड लिख रहे थे और rep ret निर्देश का उपयोग नहीं करना चाहते थे। यह a trick necessary for certain AMD processors that avoids a branch-prediction penalty है। जब तक आप एक असेंबली गुरु नहीं थे, तो शायद आप इस चाल को नहीं जानते थे। लेकिन संकलक करता है, और यह भी जानता है कि जब आप विशेष रूप से इंटेल प्रोसेसर या एएमडी प्रोसेसर की विभिन्न पीढ़ी को लक्षित कर रहे हैं तो यह आवश्यक नहीं है जिसमें यह क्विर्क नहीं है।

हालांकि, आप शाखा के बाद mov एस के बेहतर होने के बारे में सही हो सकते हैं, लेकिन आपके द्वारा सुझाए गए कारणों के लिए नहीं। आधुनिक प्रोसेसर (मुझे विश्वास है कि यह नेहलेम और बाद में है, लेकिन अगर मुझे सत्यापित करने की आवश्यकता है तो मैं इसे Agner Fog's excellent optimization guides में देखता हूं) कुछ परिस्थितियों में मैक्रो-ऑप संलयन करने में सक्षम हैं। असल में, मैक्रो-ऑप फ़्यूज़न का अर्थ है कि सीपीयू का डिकोडर पाइपलाइन के सभी चरणों में बैंडविड्थ को सहेजने, एक माइक्रो-ऑप में दो योग्य निर्देशों को जोड़ देगा। cmp या test निर्देश एक सशर्त शाखा निर्देश के बाद, जैसा कि आप test3 में देखते हैं, मैक्रो-ऑप संलयन के लिए योग्य है (वास्तव में, अन्य स्थितियां हैं जिन्हें पूरा किया जाना चाहिए, लेकिन यह कोड उन आवश्यकताओं को पूरा करता है)। cmp और je के बीच अन्य निर्देशों को शेड्यूल करना, जैसा कि आप test2 में देखते हैं, मैक्रो-ऑप संलयन असंभव बनाता है, संभावित रूप से कोड को धीरे-धीरे निष्पादित कर देता है।

तर्कसंगत रूप से, यह संकलक में एक अनुकूलन दोष है। यह सकता हैmov निर्देश पुनर्क्रमित है तुरंत cmp के बाद je जगह, स्थूल सेशन संलयन के लिए क्षमता संरक्षण:

test2a(char, int*, int*): 
    mov  DWORD PTR [rsi], 0 ; do the default initialization *first* 
    mov  DWORD PTR [rdx], 0 
    cmp  dil, 99    ; this is now followed immediately by the conditional 
    je  .L10     ; branch, making macro-op fusion possible 
    rep ret 
.L10: 
    mov  DWORD PTR [rsi], 15 
    mov  DWORD PTR [rdx], 21 
    ret 

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

तो, क्या इसका मतलब यह है कि आपको हमेशा कोड लिखना चाहिए जैसा आपने test2 में किया था?
ठीक है, नहीं, कई कारणों से:

  1. कि हमने देखा है, यह एक pessimization के बाद से अनुकूलक पैटर्न को नहीं पहचान सकता हो सकता है।
  2. आपको पठनीयता और अर्थशास्त्रीय शुद्धता पहले के लिए कोड लिखना चाहिए, केवल यह अनुकूलित करने के लिए वापस जा रहा है जब आपका प्रोफाइलर इंगित करता है कि यह वास्तव में एक बाधा है। और फिर, आपको केवल अपने कंपाइलर द्वारा उत्सर्जित ऑब्जेक्ट कोड का निरीक्षण और सत्यापन करने के बाद अनुकूलित करना चाहिए, अन्यथा आप निराशा के साथ समाप्त हो सकते हैं। (मानक "अन्यथा साबित होने तक आपके कंपाइलर पर भरोसा करें" सलाह।
  3. भले ही यह कुछ बहुत ही सरल मामलों में इष्टतम हो, फिर भी "प्रीसेट" मुहावरे सामान्य नहीं है। यदि आपका प्रारंभिक समय लेने वाला है, तो संभव होने पर इसे छोड़ना तेज हो सकता है। (here, in the context of VB 6 पर चर्चा की गई एक उदाहरण है, जहां स्ट्रिंग-मैनिपुलेशन इतनी धीमी है कि जब संभव हो तो इसे समाप्त करना वास्तव में फैंसी शाखा रहित कोड की तुलना में तेजी से निष्पादन समय में होता है। आमतौर पर, यदि आप फ़ंक्शन कॉल के आसपास शाखा में सक्षम होते हैं तो वही तर्क लागू होता है।)

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

    आप सह को फिर से लिखकर इस लागत को बचा सकते हैं डी ऐसा है कि यह एक अस्थायी रजिस्टर में अंतिम मूल्य जमा करता है, केवल अंत में स्मृति को संग्रहीत करता है, उदा।:

    test2b(char, int*, int*): 
        xor  eax, eax    ; pre-zero the EAX register 
        xor  ecx, ecx    ; pre-zero the ECX register 
        cmp  dil, 99 
        je  Done 
        mov  eax, 15    ; change the value in EAX if necessary 
        mov  ecx, 21    ; change the value in ECX if necessary 
    Done: 
        mov  DWORD PTR [rsi], eax ; store our final temp values to memory 
        mov  DWORD PTR [rdx], ecx 
        ret 
    

    लेकिन यह दो अतिरिक्त रजिस्टरों (eax और ecx) clobbers और वास्तव में तेजी से नहीं हो सकता। आपको इसे बेंचमार्क करना होगा। या संकलक को इस कोड को उत्सर्जित करने पर भरोसा करें जब यह वास्तव में इष्टतम होता है, जैसे कि जब यह एक तंग लूप के भीतर test2 जैसे फ़ंक्शन को रेखांकित करता है।

  4. भले ही आप गारंटी दे सकें कि कोड को एक निश्चित तरीके से लिखने से संकलक ब्रांचलेस कोड को उत्सर्जित कर सकता है, यह आवश्यक नहीं होगा! जबकि शाखाओं को गलत तरीके से धीमा कर दिया जाता है, गलतफहमी वास्तव में काफी दुर्लभ होती है। आधुनिक प्रोसेसर में बेहद अच्छी शाखा भविष्यवाणी इंजन हैं, जो ज्यादातर मामलों में 99% से अधिक की अनुमानित accuracies प्राप्त करते हैं।

    शाखा गलतफहमी से बचने के लिए सशर्त चाल बहुत अच्छी हैं, लेकिन उनके पास निर्भरता श्रृंखला की लंबाई बढ़ाने का महत्वपूर्ण नुकसान है। इसके विपरीत, एक सही भविष्यवाणी की गई शाखा निर्भरता श्रृंखला तोड़ती है। (शायद यही कारण है कि जब आप अतिरिक्त चर जोड़ते हैं तो जीसीसी दो सीएमओवी निर्देशों को उत्सर्जित नहीं करता है।) यदि आप शाखा भविष्यवाणी विफल होने की उम्मीद करते हैं तो एक सशर्त चाल केवल एक प्रदर्शन जीत होती है। यदि आप भविष्यवाणी की सफलता दर ~ 75% या बेहतर पर भरोसा कर सकते हैं, तो एक सशर्त शाखा शायद तेज है, क्योंकि यह निर्भरता श्रृंखला को तोड़ती है और इसकी कम विलंबता होती है। और मुझे संदेह होगा कि यह मामला यहां होगा, जब तक कि c प्रत्येक बार फ़ंक्शन कहलाए जाने पर 99 और 99-99 के बीच तेजी से आगे और पीछे बदलता है। (Agner Fog's "Optimizing subroutines in assembly language", पीपी 70-71 देखें।)

+0

वाह। एग्नेर कोहरे के महान लिंक के लिए धन्यवाद और धन्यवाद। –

+0

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

संबंधित मुद्दे

 संबंधित मुद्दे