ऊपर कार्यों के लिए, मैं ठीक उसी विधानसभा कोड (का उपयोग कर 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
में किया था?
ठीक है, नहीं, कई कारणों से:
- कि हमने देखा है, यह एक pessimization के बाद से अनुकूलक पैटर्न को नहीं पहचान सकता हो सकता है।
- आपको पठनीयता और अर्थशास्त्रीय शुद्धता पहले के लिए कोड लिखना चाहिए, केवल यह अनुकूलित करने के लिए वापस जा रहा है जब आपका प्रोफाइलर इंगित करता है कि यह वास्तव में एक बाधा है। और फिर, आपको केवल अपने कंपाइलर द्वारा उत्सर्जित ऑब्जेक्ट कोड का निरीक्षण और सत्यापन करने के बाद अनुकूलित करना चाहिए, अन्यथा आप निराशा के साथ समाप्त हो सकते हैं। (मानक "अन्यथा साबित होने तक आपके कंपाइलर पर भरोसा करें" सलाह।
भले ही यह कुछ बहुत ही सरल मामलों में इष्टतम हो, फिर भी "प्रीसेट" मुहावरे सामान्य नहीं है। यदि आपका प्रारंभिक समय लेने वाला है, तो संभव होने पर इसे छोड़ना तेज हो सकता है। (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
जैसे फ़ंक्शन को रेखांकित करता है।
भले ही आप गारंटी दे सकें कि कोड को एक निश्चित तरीके से लिखने से संकलक ब्रांचलेस कोड को उत्सर्जित कर सकता है, यह आवश्यक नहीं होगा! जबकि शाखाओं को गलत तरीके से धीमा कर दिया जाता है, गलतफहमी वास्तव में काफी दुर्लभ होती है। आधुनिक प्रोसेसर में बेहद अच्छी शाखा भविष्यवाणी इंजन हैं, जो ज्यादातर मामलों में 99% से अधिक की अनुमानित accuracies प्राप्त करते हैं।
शाखा गलतफहमी से बचने के लिए सशर्त चाल बहुत अच्छी हैं, लेकिन उनके पास निर्भरता श्रृंखला की लंबाई बढ़ाने का महत्वपूर्ण नुकसान है। इसके विपरीत, एक सही भविष्यवाणी की गई शाखा निर्भरता श्रृंखला तोड़ती है। (शायद यही कारण है कि जब आप अतिरिक्त चर जोड़ते हैं तो जीसीसी दो सीएमओवी निर्देशों को उत्सर्जित नहीं करता है।) यदि आप शाखा भविष्यवाणी विफल होने की उम्मीद करते हैं तो एक सशर्त चाल केवल एक प्रदर्शन जीत होती है। यदि आप भविष्यवाणी की सफलता दर ~ 75% या बेहतर पर भरोसा कर सकते हैं, तो एक सशर्त शाखा शायद तेज है, क्योंकि यह निर्भरता श्रृंखला को तोड़ती है और इसकी कम विलंबता होती है। और मुझे संदेह होगा कि यह मामला यहां होगा, जब तक कि c
प्रत्येक बार फ़ंक्शन कहलाए जाने पर 99 और 99-99 के बीच तेजी से आगे और पीछे बदलता है। (Agner Fog's "Optimizing subroutines in assembly language", पीपी 70-71 देखें।)
मुझे लगता है कि सवाल पर upvotes का एक बहुत की भविष्यवाणी :) –
आपको आश्चर्य है कि क्या एक्स वाई की तुलना में तेजी है, तो आप प्रोफ़ाइल चाहिए, _profile_ और __profile__। केवल सटीक समय समस्या पर कुछ प्रकाश डालेगा। – ForceBru
क्या आप पूछ रहे हैं कि कोई अन्य (प्रोफाइल) से तेज है या विधानसभा अलग क्यों है? – juanchopanza