लॉजिकल एंड ऑपरेटर ( &&
) शॉर्ट-सर्किट मूल्यांकन का उपयोग करता है, जिसका अर्थ है कि दूसरा परीक्षण केवल तभी किया जाता है जब पहली तुलना सच का मूल्यांकन करती है। यह अक्सर वही शब्दार्थ होता है जिसकी आपको आवश्यकता होती है। उदाहरण के लिए, निम्नलिखित कोड पर विचार करें:
if ((p != nullptr) && (p->first > 0))
आपको यह सुनिश्चित करना चाहिए कि आपके द्वारा इसे स्थगित करने से पहले सूचक गैर-शून्य है। यदि यह एक शॉर्ट-सर्किट मूल्यांकन नहीं था , तो आपके पास अपरिभाषित व्यवहार होगा क्योंकि आप एक अशक्त सूचक को निष्क्रिय कर रहे होंगे।
यह भी संभव है कि शॉर्ट सर्किट मूल्यांकन उन मामलों में एक प्रदर्शन लाभ देता है जहां स्थितियों का मूल्यांकन एक महंगी प्रक्रिया है। उदाहरण के लिए:
if ((DoLengthyCheck1(p) && (DoLengthyCheck2(p))
यदि DoLengthyCheck1
विफल रहता है, तो कॉल करने का कोई मतलब नहीं है DoLengthyCheck2
।
हालांकि, परिणामस्वरूप बाइनरी में, शॉर्ट-सर्किट ऑपरेशन अक्सर दो शाखाओं में परिणत होता है, क्योंकि यह कंपाइलर के लिए इन शब्दार्थों को संरक्षित करने का सबसे आसान तरीका है। (यही कारण है कि, सिक्के के दूसरी तरफ, शॉर्ट-सर्किट मूल्यांकन कभी-कभी अनुकूलन क्षमता को बाधित कर सकता है ।) आप इसे if
GCC 5.4 द्वारा अपने बयान के लिए बनाए गए ऑब्जेक्ट कोड के संबंधित हिस्से को देखकर देख सकते हैं :
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13w, 478 ; (curr[i] < 479)
ja .L5
cmp ax, 478 ; (l[i + shift] < 479)
ja .L5
add r8d, 1 ; nontopOverlap++
आप यहाँ दो तुलनाएँ ( cmp
निर्देश) यहाँ देख रहे हैं, प्रत्येक के बाद एक अलग सशर्त कूद / शाखा ( ja
या ऊपर कूदें)।
यह अंगूठे का एक सामान्य नियम है कि शाखाएं धीमी होती हैं और इसलिए उन्हें तंग छोरों से बचा जाना चाहिए। यह लगभग सभी x86 प्रोसेसर पर सच है, विनम्र 8088 से (जिनकी धीमी गति के समय और अत्यंत छोटी प्रीफ़ेच कतार [एक अनुदेश कैश के बराबर है), शाखा भविष्यवाणी की पूरी कमी के साथ संयुक्त, का मतलब है कि शाखाओं को कैश डंप होने की आवश्यकता थी ) से आधुनिक कार्यान्वयन (जिनकी लंबी पाइपलाइन गलत शाखाओं को समान रूप से महंगा बनाती हैं)। ध्यान दें कि मैं वहां फिसल गया था। पेंटियम प्रो के बाद से आधुनिक प्रोसेसर में उन्नत शाखा पूर्वानुमान इंजन हैं जो शाखाओं की लागत को कम करने के लिए डिज़ाइन किए गए हैं। यदि शाखा की दिशा का सही अनुमान लगाया जा सकता है, तो लागत न्यूनतम है। ज्यादातर समय, यह अच्छी तरह से काम करता है, लेकिन यदि आप रोग संबंधी मामलों में आते हैं, जहां शाखा भविष्यवक्ता आपकी तरफ नहीं है,आपका कोड बेहद धीमा हो सकता है । यह निश्चित रूप से आप यहाँ हैं, क्योंकि आप कहते हैं कि आपका सरणी अनसुलझा है।
आप कहते हैं कि बेंचमार्क ने पुष्टि की कि कोड के &&
साथ बदलने *
से कोड काफ़ी तेजी से होता है। इसका कारण तब स्पष्ट होता है जब हम ऑब्जेक्ट कोड के संबंधित हिस्से की तुलना करते हैं:
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
xor r15d, r15d ; (curr[i] < 479)
cmp r13w, 478
setbe r15b
xor r14d, r14d ; (l[i + shift] < 479)
cmp ax, 478
setbe r14b
imul r14d, r15d ; meld results of the two comparisons
cmp r14d, 1 ; nontopOverlap++
sbb r8d, -1
यह थोड़ा जवाबी है कि यह तेजी से हो सकता है, क्योंकि यहां अधिक निर्देश हैं, लेकिन कभी-कभी अनुकूलन काम करता है। आप एक ही तुलना ( cmp
) यहां कर रहे हैं, लेकिन अब, प्रत्येक एक से पहले है xor
और एक द्वारा पीछा किया जा रहा है setbe
। XOR एक रजिस्टर को साफ़ करने के लिए एक मानक चाल है। setbe
एक x86 निर्देश है कि एक ध्वज के मूल्य पर आधारित एक सा सेट है, और अक्सर शाखा कोड लागू करने के लिए प्रयोग किया जाता है। यहाँ, setbe
का विलोम है ja
। यदि तुलना नीचे-या-बराबर (चूंकि रजिस्टर पूर्व-शून्य था, तो यह 0 अन्यथा होगा), ja
तो यह अपने गंतव्य रजिस्टर को 1 पर सेट करता है, जबकि तुलना के ऊपर होने पर शाखा दी जाती है। एक बार इन दो मूल्यों में प्राप्त किया गया है r15b
औरr14b
रजिस्टर, वे एक साथ कई बार उपयोग किए जाते हैं imul
। गुणन पारंपरिक रूप से एक धीमी गति से संचालन था, लेकिन यह आधुनिक प्रोसेसर पर बहुत तेज़ है, और यह विशेष रूप से तेज़ होगा, क्योंकि यह केवल दो बाइट के आकार को गुणा कर रहा है।
आप बिटविंड और ऑपरेटर ( &
) के साथ गुणा को आसानी से बदल सकते हैं , जो शॉर्ट-सर्किट मूल्यांकन नहीं करता है। यह कोड को अधिक स्पष्ट बनाता है, और एक ऐसा पैटर्न है जो आम तौर पर पहचानने वाले को संकलित करता है। लेकिन जब आप अपने कोड के साथ ऐसा करते हैं और इसे जीसीसी 5.4 के साथ संकलित करते हैं, तो यह पहली शाखा का उत्सर्जन जारी रखता है:
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13w, 478 ; (curr[i] < 479)
ja .L4
cmp ax, 478 ; (l[i + shift] < 479)
setbe r14b
cmp r14d, 1 ; nontopOverlap++
sbb r8d, -1
कोई तकनीकी कारण नहीं है कि इस तरह से कोड का उत्सर्जन करना था, लेकिन किसी कारण से, इसके आंतरिक उत्तराधिकार यह बता रहे हैं कि यह तेज है। शाखा सूचक आपके पक्ष में था, तो यह संभवत: तेज़ होगा , लेकिन यदि शाखा की भविष्यवाणी सफल होने की तुलना में अधिक बार विफल हो जाती है तो यह धीमी हो जाएगी।
संकलक (और अन्य संकलक, जैसे क्लैंग) की नई पीढ़ी इस नियम को जानती है, और कभी-कभी इसका उपयोग उसी कोड को उत्पन्न करने के लिए करेगी जिसे आपने हाथ से अनुकूलन करके मांगा होगा। मैं नियमित रूप से क्लैंग अनुवाद के &&
भावों को उसी कोड में देखता हूं जो अगर मैंने उपयोग किया होता तो उत्सर्जित हो जाता &
। सामान्य &&
ऑपरेटर का उपयोग करके आपके कोड के साथ GCC 6.2 से प्रासंगिक आउटपुट निम्न है :
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13d, 478 ; (curr[i] < 479)
jg .L7
xor r14d, r14d ; (l[i + shift] < 479)
cmp eax, 478
setle r14b
add esi, r14d ; nontopOverlap++
ध्यान दें कि यह कितना चालाक है! यह हस्ताक्षर किए शर्तों का उपयोग किया जाता है ( jg
और setle
) के रूप में अहस्ताक्षरित की स्थिति (करने का विरोध किया ja
और setbe
), लेकिन यह महत्वपूर्ण नहीं है। आप देख सकते हैं कि यह अभी भी पुराने संस्करण की तरह पहली स्थिति के लिए तुलना-और-शाखा करता है, और setCC
दूसरी स्थिति के लिए शाखाहीन कोड उत्पन्न करने के लिए एक ही निर्देश का उपयोग करता है , लेकिन यह वृद्धि कैसे करता है, इसमें बहुत अधिक कुशल है। । एक sbb
ऑपरेशन के लिए झंडे सेट करने की तुलना में एक दूसरा, निरर्थक तुलना करने के बजाय , यह उस ज्ञान का उपयोग करता है जो r14d
या तो बिना किसी शर्त के इस मूल्य को जोड़ने के लिए 1 या 0 होगा nontopOverlap
। यदि r14d
0 है, तो जोड़ एक विकल्प नहीं है; अन्यथा, यह 1 जोड़ता है, ठीक उसी तरह जैसे यह करना है।
जीसीसी 6.2 वास्तव में बिटकॉइन ऑपरेटर की तुलना में शॉर्ट-सर्कुलेटिंग ऑपरेटर का उपयोग करते समय अधिक कुशल कोड का उत्पादन करता है :&&
&
movzx r13d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r13d, 478 ; (curr[i] < 479)
jg .L6
cmp eax, 478 ; (l[i + shift] < 479)
setle r14b
cmp r14b, 1 ; nontopOverlap++
sbb esi, -1
शाखा और सशर्त सेट अभी भी हैं, लेकिन अब यह वेतन वृद्धि के कम चतुर तरीके से वापस लौटता है nontopOverlap
। यह एक महत्वपूर्ण सबक है कि आपको अपने कंपाइलर को आउट-चालाक करने की कोशिश करते समय सावधान रहना चाहिए!
लेकिन अगर आप बेंचमार्क के साथ साबित कर सकते हैं कि ब्रांचिंग कोड वास्तव में धीमा है, तो यह आपके कंपाइलर को आज़माने और चतुर करने के लिए भुगतान कर सकता है। आपको बस डिस्सैड के सावधानीपूर्वक निरीक्षण के साथ ऐसा करना है - और जब आप कंपाइलर के बाद के संस्करण में अपग्रेड करते हैं, तो अपने निर्णयों का पुनर्मूल्यांकन करने के लिए तैयार रहें। उदाहरण के लिए, आपके पास कोड को फिर से लिखा जा सकता है:
nontopOverlap += ((curr[i] < 479) & (l[i + shift] < 479));
if
यहाँ कोई बयान नहीं दिया गया है, और कंपाइलरों के विशाल बहुमत ने इसके लिए ब्रांचिंग कोड को छोड़ने के बारे में कभी नहीं सोचा होगा। जीसीसी कोई अपवाद नहीं है; सभी संस्करण निम्नलिखित के लिए कुछ समान उत्पन्न करते हैं:
movzx r14d, WORD PTR [rbp+rcx*2]
movzx eax, WORD PTR [rbx+rcx*2]
cmp r14d, 478 ; (curr[i] < 479)
setle r15b
xor r13d, r13d ; (l[i + shift] < 479)
cmp eax, 478
setle r13b
and r13d, r15d ; meld results of the two comparisons
add esi, r13d ; nontopOverlap++
यदि आप पिछले उदाहरणों के साथ अनुसरण कर रहे हैं, तो यह आपको बहुत परिचित होना चाहिए। दोनों तुलना एक शाखाहीन तरीके से की जाती है, मध्यवर्ती परिणाम and
एक साथ संपादित होते हैं, और फिर यह परिणाम (जो या तो 0 या 1 होगा) को add
संपादित किया जाता है nontopOverlap
। यदि आप शाखा रहित कोड चाहते हैं, तो यह वस्तुतः यह सुनिश्चित करेगा कि आप इसे प्राप्त करें।
जीसीसी 7 ने और भी स्मार्ट हो गया है। यह अब मूल कोड के रूप में उपरोक्त चाल के लिए लगभग समान कोड (निर्देशों के कुछ मामूली पुनर्विकास को छोड़कर) उत्पन्न करता है। तो, आपके प्रश्न का उत्तर, "संकलक इस तरह से व्यवहार क्यों करता है?" , शायद इसलिए कि वे परिपूर्ण नहीं हैं! वे सबसे इष्टतम कोड को संभव बनाने के लिए उत्तराधिकारियों का उपयोग करने की कोशिश करते हैं, लेकिन वे हमेशा सबसे अच्छा निर्णय नहीं लेते हैं। लेकिन कम से कम वे समय के साथ होशियार हो सकते हैं!
इस स्थिति को देखने का एक तरीका यह है कि ब्रांचिंग कोड में सबसे बेहतर स्थिति है । यदि शाखा की भविष्यवाणी सफल होती है, तो अनावश्यक ऑपरेशनों को छोड़ देने से परिणाम तेजी से भागेंगे। हालांकि, शाखा रहित कोड में सबसे खराब स्थिति है । यदि शाखा की भविष्यवाणी विफल हो जाती है, तो शाखा से बचने के लिए आवश्यक कुछ अतिरिक्त निर्देशों को निष्पादित करना निश्चित रूप से एक गलत शाखा से तेज होगा । यहां तक कि कंपाइलर के सबसे स्मार्ट और सबसे चालाक के पास इस चुनाव को बनाने में कठिन समय होगा।
और आपके सवाल के लिए कि क्या यह कुछ प्रोग्रामर के लिए बाहर देखने की जरूरत है, जवाब लगभग निश्चित रूप से नहीं है, कुछ हॉट लूप्स को छोड़कर जो आप माइक्रो-ऑप्टिमाइज़ेशन के माध्यम से तेज करने की कोशिश कर रहे हैं। फिर, आप डिससैस के साथ बैठते हैं और इसे ट्वीक करने के तरीके ढूंढते हैं। और, जैसा कि मैंने पहले कहा, उन फैसलों को फिर से तैयार करने के लिए तैयार रहें जब आप संकलक के एक नए संस्करण के लिए अद्यतन करते हैं, क्योंकि यह या तो आपके मुश्किल कोड के साथ कुछ बेवकूफी कर सकता है, या हो सकता है कि उसने अपने अनुकूलन उत्तराधिकारियों को पर्याप्त बदल दिया हो ताकि आप वापस जा सकें अपने मूल कोड का उपयोग करने के लिए। अच्छी तरह से टिप्पणी करें!