यदि आपको लगता है कि 64-बिट DIV निर्देश दो से विभाजित करने का एक अच्छा तरीका है, तो कोई आश्चर्य नहीं कि कंपाइलर का एसएसएम आउटपुट आपके हाथ से लिखे गए कोड को हरा देता है, यहां तक कि -O0
(संकलन तेज, कोई अतिरिक्त अनुकूलन नहीं, और मेमोरी के बाद / पुनः लोड करने के लिए / प्रत्येक सी स्टेटमेंट से पहले एक डिबगर चर को संशोधित कर सकता है)।
कुशल एएसएम लिखने का तरीका जानने के लिए एग्नेर फॉग के ऑप्टिमाइज़िंग असेंबली गाइड देखें । उसके पास निर्देश सारणी और विशिष्ट सीपीयू के लिए विशिष्ट विवरण के लिए एक माइक्रो-गाइड गाइड भी है। यह भी देखें86 टैग अधिक पूर्ण लिंक के लिए विकी।
संकलक को हाथ से लिखे हुए आसन से पिटाई के बारे में यह सामान्य प्रश्न भी देखें: क्या इनलाइन असेंबली भाषा देशी C ++ कोड से धीमी है? । TL: DR: हाँ अगर आप इसे गलत करते हैं (जैसे यह प्रश्न)।
आमतौर पर आप ठीक कर रहे हैं संकलक अपनी बात करते हैं, खासकर यदि आप C ++ लिखने की कोशिश करते हैं जो कुशलता से संकलित कर सकते हैं । यह भी देखें कि संकलित भाषाओं की तुलना में विधानसभा तेज है? । इन सी स्लाइड के उत्तर में से एक यह दिखाता है कि विभिन्न सी कंपाइलर शांत चाल के साथ कुछ बहुत ही सरल कार्यों का अनुकूलन करते हैं। मैट गॉडबोल्ट की CppCon2017 में बात " मेरे साथी ने मेरे लिए क्या किया? संकलक के ढक्कन को खोलना "एक समान नस में है।
even:
mov rbx, 2
xor rdx, rdx
div rbx
इंटेल हैसवेल पर, div r64
36 यूओपी है, जिसमें 32-96 चक्रों की विलंबता है , और प्रति 21-74 चक्रों में से एक थ्रूपुट है। (प्लस आरबीएक्स और शून्य आरडीएक्स स्थापित करने के लिए 2 यूओपीएस, लेकिन आउट-ऑफ-ऑर्डर निष्पादन उन लोगों को जल्दी चला सकता है)। DIV जैसे उच्च-यूओपी-गिनती निर्देश माइक्रोकोडेड हैं, जो फ्रंट-एंड अड़चनों का कारण भी बन सकते हैं। इस मामले में, विलंबता सबसे प्रासंगिक कारक है क्योंकि यह एक लूप-आधारित निर्भरता श्रृंखला का हिस्सा है।
shr rax, 1
समान अहस्ताक्षरित विभाजन करता है: यह 1 uop है, 1c विलंबता के साथ , और प्रति घड़ी चक्र 2 चला सकता है।
तुलना के लिए, 32-बिट डिवीजन तेज है, लेकिन अभी भी भयानक बनाम बदलाव है। idiv r32
9 उफ, 22-29c विलंबता है, और हसवेल पर 8-11 सी थ्रूपुट प्रति एक है।
जैसा कि आप जीसीसी के -O0
एएसएम आउटपुट ( गॉडबोल्ट कंपाइलर एक्सप्लोरर ) को देखने से देख सकते हैं , यह केवल पाली के निर्देशों का उपयोग करता है । बजना -O0
संकलन भोलेपन से है जैसे आप सोचा था कि, यहां तक कि दो बार 64-बिट IDIV का उपयोग करता है। (जब अनुकूलन, संकलक IDIV के दोनों आउटपुट का उपयोग करते हैं जब स्रोत एक ही ऑपरेंड के साथ एक डिवीजन और मापांक करता है, अगर वे IDIV का उपयोग करते हैं)
जीसीसी में पूरी तरह से अनुभवहीन मोड नहीं है; यह हमेशा GIMPLE के माध्यम से बदल जाता है, जिसका अर्थ है कि कुछ "अनुकूलन" को अक्षम नहीं किया जा सकता है । इसमें IDIV से बचने के लिए डिवीजन-बाय-कॉन्स्टेंट और शिफ्ट्स (2 की शक्ति) या एक निश्चित-बिंदु गुणक व्युत्क्रम (2 की गैर शक्ति) का उपयोग करना शामिल है ( div_by_13
उपरोक्त गॉडबोल लिंक में देखें)।
gcc -Os
(आकार के लिए ऑप्टिमाइज़ करें ) गैर-पॉवर ऑफ़ -2 डिवीज़न के लिए IDIV का उपयोग करता है , दुर्भाग्य से उन मामलों में भी जहां गुणक व्युत्क्रम कोड केवल थोड़ा बड़ा है, लेकिन बहुत तेज़ है।
कंपाइलर की मदद करना
(इस मामले के लिए सारांश: उपयोग करें uint64_t n
)
सबसे पहले, यह केवल अनुकूलित संकलक आउटपुट को देखने के लिए दिलचस्प है। ( -O3
)। -O0
गति मूल रूप से अर्थहीन है।
अपने asm आउटपुट को देखें (Godbolt पर, या देखें कि GCC / clang विधानसभा आउटपुट से "शोर" कैसे निकालें? )। जब कंपाइलर पहली बार में इष्टतम कोड नहीं बनाता है: अपने सी / सी ++ स्रोत को इस तरह से लिखना जो कंपाइलर को बेहतर कोड बनाने में मार्गदर्शन करता है, आमतौर पर सबसे अच्छा तरीका है । आपको asm को जानना है, और जानना है कि क्या कुशल है, लेकिन आप इस ज्ञान को अप्रत्यक्ष रूप से लागू करते हैं। कंपाइलर भी विचारों का एक अच्छा स्रोत हैं: कभी-कभी क्लैंग कुछ शांत कर देगा, और आप एक ही काम करने में जीसीसी को हाथ से पकड़ सकते हैं: इस उत्तर को देखें और मैंने नीचे @ वैडरैक के कोड में गैर-अनियंत्रित लूप के साथ क्या किया।
यह दृष्टिकोण पोर्टेबल है, और 20 वर्षों में कुछ भविष्य के कंपाइलर इसे भविष्य के हार्डवेयर (x86 या नहीं) पर कुशल होने के लिए संकलित कर सकते हैं, शायद नए आईएसए एक्सटेंशन या ऑटो-वेक्टरिंग का उपयोग कर रहे हैं। 15 साल पहले से लिखे गए x86-64 asm आमतौर पर Skylake के लिए आमतौर पर ट्यून नहीं किए जाएंगे। उदाहरण के लिए तुलना करें और शाखा मैक्रो-फ्यूजन वापस मौजूद नहीं था। एक माइक्रोआर्किटेक्चर के लिए हाथ से तैयार किए गए एएसएम के लिए अब क्या इष्टतम है जो अन्य वर्तमान और भविष्य के सीपीयू के लिए इष्टतम नहीं हो सकता है। @ Johnfound के जवाब पर टिप्पणियाँ AMD Bulldozer और Intel Haswell के बीच प्रमुख अंतरों पर चर्चा करती हैं, जो इस कोड पर एक बड़ा प्रभाव डालती हैं। लेकिन सिद्धांत रूप में, g++ -O3 -march=bdver3
और g++ -O3 -march=skylake
सही काम करेंगे। (या -march=native
।) या -mtune=...
अन्य सीपीयू का समर्थन नहीं करने वाले निर्देशों का उपयोग किए बिना, बस धुन करने के लिए।
मेरी भावना यह है कि संकलक को यह बताने के लिए कि वर्तमान सीपीयू आपके लिए अच्छा है, भविष्य के संकलक के लिए समस्या नहीं होनी चाहिए। वे उम्मीद कर रहे हैं कि कोड बदलने के तरीके खोजने में वर्तमान संकलक से बेहतर है, और भविष्य के सीपीयू के लिए काम करने का तरीका खोज सकते हैं। भले ही, भविष्य x86 वर्तमान x86 पर कुछ भी अच्छा नहीं होगा, और भविष्य के संकलक आपके सी स्रोत से डेटा आंदोलन की तरह कुछ लागू करते समय किसी भी विशेष-विशिष्ट नुकसान से बचेंगे, अगर यह कुछ बेहतर नहीं दिखता है।
हाथ से लिखा asm अनुकूलक के लिए एक ब्लैक-बॉक्स है, इसलिए जब एक इनपुट एक संकलन-समय स्थिर बनाता है तो निरंतर-प्रसार काम नहीं करता है। अन्य अनुकूलन भी प्रभावित होते हैं। Asm का उपयोग करने से पहले https://gcc.gnu.org/wiki/DontUseInlineAsm पढ़ें । (और MSVC- स्टाइल इनलाइन asm से बचें: इनपुट / आउटपुट को मेमोरी से गुजरना पड़ता है जो ओवरहेड जोड़ता है ।)
इस स्थिति में : आपके n
पास एक हस्ताक्षरित प्रकार है, और gcc SAR / SHR / ADD अनुक्रम का उपयोग करता है जो सही गोलाई देता है। (IDIV और अंकगणितीय-शिफ्ट "राउंड" को नकारात्मक इनपुट के लिए अलग-अलग रूप से देखें, एसएआर इनस सेट रेफरी मैनुअल प्रविष्टि देखें )। (IDK अगर gcc ने कोशिश की और यह साबित करने में विफल रहा कि n
नकारात्मक नहीं हो सकता है, या क्या है। हस्ताक्षर-अतिप्रवाह अपरिभाषित व्यवहार है, इसलिए इसे करने में सक्षम होना चाहिए।)
आपको उपयोग करना चाहिए था uint64_t n
, इसलिए यह सिर्फ SHR कर सकता है। और इसलिए यह उन प्रणालियों के लिए पोर्टेबल है जहां long
केवल 32-बिट (जैसे x86-64 विंडोज) है।
BTW, gcc का अनुकूलित asm आउटपुट बहुत अच्छा लगता है (उपयोग करते हुए unsigned long n
) : इनर लूप इसे इनलाइन main()
करता है:
# from gcc5.4 -O3 plus my comments
# edx= count=1
# rax= uint64_t n
.L9: # do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
mov rdi, rax
shr rdi # rdi = n>>1;
test al, 1 # set flags based on n%2 (aka n&1)
mov rax, rcx
cmove rax, rdi # n= (n%2) ? 3*n+1 : n/2;
add edx, 1 # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
cmp/branch to update max and maxi, and then do the next n
आंतरिक लूप शाखा रहित होता है, और लूप-आधारित निर्भरता श्रृंखला का महत्वपूर्ण पथ है:
- 3-घटक LEA (3 चक्र)
- सेमीोव (हसवेल पर 2 चक्र, ब्रॉडवेल या बाद में 1 सी)।
कुल: पुनरावृत्ति प्रति 5 चक्र, विलंबता अड़चन । आउट-ऑफ-ऑर्डर निष्पादन इसके साथ समानांतर में सब कुछ का ख्याल रखता है (सिद्धांत में: मैंने यह देखने के लिए पूर्ण काउंटरों के साथ परीक्षण नहीं किया है कि क्या यह वास्तव में 5c / iter पर चलता है)।
के झंडे इनपुट cmov
(टेस्ट द्वारा उत्पादित), तेजी से RAX इनपुट से उत्पादन करने के लिए (LEA-> MOV से) है, इसलिए यह महत्वपूर्ण मार्ग पर नहीं है।
इसी तरह, CMV का RDI इनपुट बनाने वाला MOV-> SHR महत्वपूर्ण पथ से दूर है, क्योंकि यह LEA से भी तेज है। IvyBridge पर MOV और बाद में शून्य विलंबता (रजिस्टर-नाम बदलने के समय संभाला)। (यह अभी भी पाइप में एक यूओपी, और एक स्लॉट लेता है, इसलिए यह मुफ़्त नहीं है, बस शून्य विलंबता)। LEA dep श्रृंखला में अतिरिक्त MOV अन्य CPU पर अड़चन का हिस्सा है।
Cmp / jne भी महत्वपूर्ण पथ का हिस्सा नहीं है: यह पाश-चालित नहीं है, क्योंकि नियंत्रण आश्रितों को महत्वपूर्ण पथ पर डेटा निर्भरता के विपरीत शाखा भविष्यवाणी + सट्टा निष्पादन के साथ नियंत्रित किया जाता है।
संकलक की पिटाई
जीसीसी ने यहां बहुत अच्छा काम किया। यह inc edx
इसके बजाय काadd edx, 1
उपयोग करके एक कोड बाइट को बचा सकता है , क्योंकि किसी को भी P4 और आंशिक-ध्वज-संशोधित निर्देशों के लिए इसकी झूठी-निर्भरता की परवाह नहीं है।
यह भी सभी MOV निर्देश, और टेस्ट बचा सकता है: SHR सीएफ = बिट बाहर स्थानांतरित कर दिया है, तो हम उपयोग कर सकते हैं सेट cmovc
के बजाय test
/ cmovz
।
### Hand-optimized version of what gcc does
.L9: #do{
lea rcx, [rax+1+rax*2] # rcx = 3*n + 1
shr rax, 1 # n>>=1; CF = n&1 = n%2
cmovc rax, rcx # n= (n&1) ? 3*n+1 : n/2;
inc edx # ++count;
cmp rax, 1
jne .L9 #}while(n!=1)
एक अन्य चतुर चाल के लिए @ जॉन्फाउंड का जवाब देखें: SHR के ध्वज परिणाम पर शाखा द्वारा CMP को हटाने के साथ-साथ CMOV के लिए इसका उपयोग करना: शून्य केवल अगर n 1 (या 0) के साथ शुरू करना था। (मज़ेदार तथ्य: SHR काउंट के साथ! = 1 पर नेहलेम या इससे पहले यदि आप झंडा परिणाम पढ़ते हैं तो एक स्टाल होता है । इस तरह से उन्होंने इसे एकल बनाया है। शिफ्ट-बाय -1 विशेष एन्कोडिंग ठीक है, हालांकि।)
MOV से बचना हसवेल पर विलंबता के साथ मदद नहीं करता है ( क्या x86 का MOV वास्तव में "मुक्त" हो सकता है? मैं इसे क्यों नहीं पुन: पेश कर सकता हूं? )। यह इंटेल प्री-आईवीबी, और एएमडी बुलडोजर-परिवार जैसे सीपीयू पर काफी मदद करता है , जहां एमओवी शून्य-विलंबता नहीं है। संकलक के बर्बाद किए गए MOV निर्देश महत्वपूर्ण पथ को प्रभावित करते हैं। BD का जटिल-LEA और CMOV दोनों निम्न विलंबता (क्रमशः 2c और 1c) हैं, इसलिए यह विलंबता का एक बड़ा अंश है। इसके अलावा, थ्रूपुट बाधाएं एक मुद्दा बन जाती हैं, क्योंकि इसमें केवल दो पूर्णांक ALU पाइप हैं। @ जॉनफाउंड के उत्तर को देखें , जहां उनके पास एक एएमडी सीपीयू से परिणाम हैं।
हसवेल पर भी, यह संस्करण कुछ समय की देरी से बचने में थोड़ी मदद कर सकता है, जहां एक गैर-महत्वपूर्ण यूओपी महत्वपूर्ण पथ पर एक से एक निष्पादन पोर्ट चुराता है, 1 चक्र से निष्पादन में देरी करता है। (इसे संसाधन संघर्ष कहा जाता है)। यह एक रजिस्टर को भी बचाता है, जो n
एक interleaved पाश में समानांतर में कई मूल्यों को करते समय मदद कर सकता है (नीचे देखें)।
LEA की लेटेंसी इंटेल एसएनबी-परिवार सीपीयू पर एड्रेसिंग मोड पर निर्भर करती है । 3 घटकों के लिए 3 सी ( [base+idx+const]
, जो दो अलग-अलग जोड़ता है), लेकिन 2 या उससे कम घटकों (एक ऐड) के साथ केवल 1 सी। कुछ CPU (जैसे Core2) एक चक्र में 3-घटक LEA भी करते हैं, लेकिन SnB- परिवार नहीं करता है। इससे भी बदतर, इंटेल SnB- परिवार अक्षांशों का मानकीकरण करता है, इसलिए 2c उप्स नहीं हैं , अन्यथा 3-घटक LEA केवल बुलडोजर की तरह 2c होगा। (3-घटक एलईए एएमडी पर धीमी है, बस उतना नहीं)।
इसलिए lea rcx, [rax + rax*2]
/ inc rcx
केवल 2 सी विलंबता है, lea rcx, [rax + rax*2 + 1]
इंटेल एसएलबी-परिवार सीपीयू पर, हसवेल की तुलना में तेज है । ब्रेक-बीडी पर भी, और कोर 2 पर भी बदतर। इसमें एक अतिरिक्त यूओपी खर्च होता है, जो सामान्य तौर पर 1 सी विलंबता को बचाने के लिए इसके लायक नहीं होता है, लेकिन विलंबता यहां प्रमुख अड़चन है और अतिरिक्त यूओपी थ्रूपुट को संभालने के लिए हैसवेल में एक विस्तृत पाइपलाइन है।
ना तो gcc, icc, और ना ही clang (Godbolt पर) SHR के CF आउटपुट का इस्तेमाल किया, हमेशा AND और TEST का उपयोग किया । सिली कंपाइलर। : P वे जटिल मशीनरी के महान टुकड़े हैं, लेकिन एक चतुर मानव अक्सर उन्हें छोटे स्तर की समस्याओं पर हरा सकता है। (निश्चित रूप से इसके बारे में सोचने के लिए हजारों से लाखों गुना अधिक समय दिया जाता है! कंपाइलर चीजों को करने के लिए हर संभव तरीके की खोज करने के लिए थकाऊ एल्गोरिदम का उपयोग नहीं करते हैं, क्योंकि बहुत सारे इनलाइन कोड का अनुकूलन करते समय बहुत लंबा समय लगेगा, जो कि है वे सबसे अच्छा करते हैं। वे लक्ष्य माइक्रोआर्किटेक्चर में पाइपलाइन का मॉडल नहीं बनाते हैं, कम से कम आईएसीए या अन्य स्थैतिक-विश्लेषण उपकरणों के समान विस्तार में नहीं हैं ; वे सिर्फ कुछ आंकड़ों का उपयोग करते हैं।)
सरल लूप अनियंत्रित करने में मदद नहीं करेगा ; यह लूप लूप-निर्भर निर्भरता श्रृंखला के विलंब पर लूप ओवरहेड / थ्रूपुट पर नहीं होता है। इसका मतलब यह है कि यह हाइपरथ्रेडिंग (या किसी अन्य प्रकार के एसएमटी) के साथ अच्छी तरह से करेगा, क्योंकि सीपीयू के पास दो थ्रेड्स से निर्देशों को हस्तक्षेप करने के लिए बहुत समय है। इसका मतलब होगा कि लूप को समानांतर में रखना main
, लेकिन यह ठीक है क्योंकि प्रत्येक थ्रेड केवल n
मानों की एक श्रृंखला की जांच कर सकता है और परिणामस्वरूप परिणामस्वरूप पूर्णांक की एक जोड़ी का उत्पादन कर सकता है।
एक ही धागे के भीतर हाथ से अंतर्क्रिया करना भी व्यवहार्य हो सकता है । हो सकता है कि समानांतर में संख्याओं की एक जोड़ी के लिए अनुक्रम की गणना करें, क्योंकि हर एक केवल एक युगल रजिस्टर लेता है, और वे सभी एक ही अपडेट कर सकते हैं max
/ maxi
। यह अधिक अनुदेश-स्तरीय समानता बनाता है ।
चाल यह तय कर रही है कि क्या शुरू करने के लिए दूसरे मूल्यों की जोड़ी पाने से पहले सभी n
मूल्यों तक पहुंचने तक इंतजार करना है , या क्या बाहर निकलना है और दूसरे क्रम के लिए रजिस्टरों को छूने के बिना, केवल एक शर्त के लिए एक नया प्रारंभ बिंदु प्राप्त करना है। संभवतः यह उपयोगी डेटा पर प्रत्येक श्रृंखला को रखने के लिए सबसे अच्छा है, अन्यथा आपको इसके काउंटर को सशर्त रूप से बढ़ाना होगा।1
n
आप शायद SSE पैक्ड-तुलना वाले सामान के साथ ऐसा कर सकते हैं जो वेक्टर तत्वों के लिए काउंटर को सशर्त रूप से बढ़ाने के लिए जहां अभी तक n
नहीं पहुंचा था 1
। और फिर एक SIMD सशर्त-वृद्धि कार्यान्वयन के समान लंबे समय तक विलंबता को छिपाने के लिए, आपको n
हवा में मूल्यों के अधिक वैक्टर रखने की आवश्यकता होगी । शायद केवल 256 बी वेक्टर (4x uint64_t
) के साथ लायक है ।
मुझे लगता है कि 1
"चिपचिपा" का पता लगाने के लिए सबसे अच्छी रणनीति उन सभी के वेक्टर को मुखौटा करना है जो आप काउंटर को बढ़ाने के लिए जोड़ते हैं। इसलिए जब आपने 1
एक तत्व में देखा है , तो वृद्धि-वेक्टर में एक शून्य होगा, और + = 0 एक शून्य है।
मैनुअल वेक्टराइजेशन के लिए अनकहा विचार
# starting with YMM0 = [ n_d, n_c, n_b, n_a ] (64-bit elements)
# ymm4 = _mm256_set1_epi64x(1): increment vector
# ymm5 = all-zeros: count vector
.inner_loop:
vpaddq ymm1, ymm0, xmm0
vpaddq ymm1, ymm1, xmm0
vpaddq ymm1, ymm1, set1_epi64(1) # ymm1= 3*n + 1. Maybe could do this more efficiently?
vprllq ymm3, ymm0, 63 # shift bit 1 to the sign bit
vpsrlq ymm0, ymm0, 1 # n /= 2
# FP blend between integer insns may cost extra bypass latency, but integer blends don't have 1 bit controlling a whole qword.
vpblendvpd ymm0, ymm0, ymm1, ymm3 # variable blend controlled by the sign bit of each 64-bit element. I might have the source operands backwards, I always have to look this up.
# ymm0 = updated n in each element.
vpcmpeqq ymm1, ymm0, set1_epi64(1)
vpandn ymm4, ymm1, ymm4 # zero out elements of ymm4 where the compare was true
vpaddq ymm5, ymm5, ymm4 # count++ in elements where n has never been == 1
vptest ymm4, ymm4
jnz .inner_loop
# Fall through when all the n values have reached 1 at some point, and our increment vector is all-zero
vextracti128 ymm0, ymm5, 1
vpmaxq .... crap this doesn't exist
# Actually just delay doing a horizontal max until the very very end. But you need some way to record max and maxi.
आप इसे हाथ से लिखे हुए asm के बजाय आंतरिक रूप से लागू कर सकते हैं।
एल्गोरिथम / कार्यान्वयन सुधार:
केवल अधिक कुशल asm के साथ एक ही तर्क को लागू करने के अलावा, तर्क को सरल बनाने के तरीके की तलाश करें, या अनावश्यक कार्य से बचें। उदाहरण के लिए दृश्यों के लिए सामान्य अंत का पता लगाने के लिए याद रखें। या इससे भी बेहतर, एक बार में 8 अनुगामी बिट्स देखें (gnasher का उत्तर)
@ ईओएफ बताता है कि tzcnt
(या bsf
) n/=2
एक कदम में कई पुनरावृत्तियों को करने के लिए इस्तेमाल किया जा सकता है । यह शायद SIMD वेक्टरिंग से बेहतर है; कोई SSE या AVX निर्देश ऐसा नहीं कर सकता है। यह अभी भी n
अलग-अलग पूर्णांक रजिस्टरों में समानांतर में कई स्केलर एस करने के साथ संगत है , हालांकि।
तो लूप इस तरह दिख सकता है:
goto loop_entry; // C++ structured like the asm, for illustration only
do {
n = n*3 + 1;
loop_entry:
shift = _tzcnt_u64(n);
n >>= shift;
count += shift;
} while(n != 1);
यह काफी कम पुनरावृत्तियों को कर सकता है, लेकिन BMI2 के बिना इंटेल SnB-परिवार CPU पर परिवर्तनशील-गणना शिफ्ट धीमी है। 3 उफ़, 2 सी विलंबता। (उनका FLAGS पर इनपुट निर्भरता है क्योंकि गिनती = 0 का अर्थ है कि झंडे अनमॉडिफ़ाइड हैं। वे इसे डेटा निर्भरता के रूप में संभालते हैं, और कई यूओपी लेते हैं क्योंकि एक यूओपी में केवल 2 इनपुट हो सकते हैं (पूर्व-एचएसडब्ल्यू / बीडीडब्ल्यू वैसे भी)। यह वह प्रकार है जो x86 के क्रेजी-CISC डिजाइन के बारे में शिकायत करने वाले लोगों को संदर्भित करता है। यह x86 सीपीयू की तुलना में धीमा बनाता है अगर वे आईएसए को खरोंच से डिजाइन करते थे, तो भी ज्यादातर इसी तरह से। (यानी यह "x86 टैक्स" का एक हिस्सा है, जिसमें गति / शक्ति खर्च होती है।) SHRX / SHLX / SARX (BMI2) एक बड़ी जीत (1 uop / 1c विलंबता) है।
यह महत्वपूर्ण मार्ग पर tzcnt (Hasc और बाद में 3c) भी डालता है, इसलिए यह लूप-आधारित निर्भरता श्रृंखला की कुल विलंबता को लंबा करता है। यह एक CMOV के लिए, या एक रजिस्टर होल्डिंग तैयार करने के लिए किसी भी आवश्यकता को हटा देता है n>>1
, हालांकि। @ वेडरक का जवाब कई पुनरावृत्तियों के लिए tzcnt / पारी को हटाकर यह सब खत्म कर देता है, जो कि अत्यधिक प्रभावी है (नीचे देखें)।
हम सुरक्षित रूप से BSF या TZCNT का उपयोग कर सकते हैं , क्योंकि n
उस बिंदु पर कभी भी शून्य नहीं किया जा सकता है। TZCNT का मशीन कोड CPU पर BSF है जो BMI1 का समर्थन नहीं करता है। (अर्थहीन उपसर्गों को अनदेखा किया जाता है, इसलिए REP BSF BSF के रूप में चलता है)।
TZCNT एएमडी सीपीयू पर बीएसएफ की तुलना में बेहतर प्रदर्शन करता है जो इसका समर्थन करते हैं, इसलिए यह उपयोग करने के लिए एक अच्छा विचार हो सकता है REP BSF
, भले ही आप जेडएफ को सेट करने के बारे में परवाह न करें यदि इनपुट आउटपुट के बजाय शून्य है। कुछ संकलक ऐसा करते हैं जब आप के __builtin_ctzll
साथ भी उपयोग करते हैं -mno-bmi
।
वे इंटेल सीपीयू पर एक ही प्रदर्शन करते हैं, इसलिए बस बाइट को बचाएं यदि यह सब मायने रखता है। इंटेल (पूर्व-स्काईलेक) पर TZCNT में अभी भी बीएसएफ की तरह ही अप्रत्यक्ष रूप से व्यवहार का समर्थन करने के लिए बीएसएफ की तरह ही कथित तौर पर केवल आउटपुट ऑपरेंड पर झूठा-निर्भरता है, क्योंकि इनपुट = 0 के साथ बीएसएफ अपने गंतव्य को बिना अनुमति के छोड़ देता है। इसलिए आपको इसके चारों ओर काम करने की ज़रूरत है जब तक कि केवल स्काईलेक के लिए अनुकूलन न हो, इसलिए अतिरिक्त आरईपी बाइट से कुछ भी हासिल नहीं करना है। (इंटेल अक्सर x86 ISA मैनुअल की आवश्यकता के ऊपर और परे जाता है, व्यापक रूप से उपयोग किए जाने वाले कोड को तोड़ने से बचने के लिए जो कुछ ऐसा नहीं होना चाहिए, या जो कि अप्रत्यक्ष रूप से अस्वीकृत है। जैसे विंडोज 9x टीएलबी प्रविष्टियों की कोई सट्टा पूर्व निर्धारित नहीं मानता है , जो सुरक्षित था। जब कोड लिखा गया था, उससे पहले इंटेल ने टीएलबी प्रबंधन नियमों को अपडेट किया था ।)
वैसे भी, हैज़वेल पर LZCNT / TZCNT का POPCNT जैसा ही गलत चित्रण है: इस प्रश्नोत्तर को देखें । यही कारण है कि @ Veedrac के कोड के लिए gcc के asm आउटपुट में, आप इसे x -zeroing के साथ डिप चेन को तोड़ते हुए देखते हैं, यह उस पर TZCNT के गंतव्य के रूप में उपयोग करने के बारे में है जब यह dst = src का उपयोग नहीं करता है। चूंकि TZCNT / LZCNT / POPCNT कभी भी अपने गंतव्य को अपरिभाषित या अपरिष्कृत नहीं छोड़ते हैं, Intel CPU पर आउटपुट पर यह गलत निर्भरता एक प्रदर्शन बग / सीमा है। संभवत: यह कुछ ट्रांजिस्टर / शक्ति के लायक है जो उन्हें अन्य यूओपी की तरह व्यवहार करता है जो एक ही निष्पादन इकाई में जाते हैं। एकमात्र अपसाइड एक अन्य यूरार्क सीमा के साथ बातचीत है: वे एक इंडेक्सिंग मोड के साथ मेमोरी ऑपरेंड को माइक्रो-फ्यूज कर सकते हैं हैसवेल पर, लेकिन स्काइलेक पर, जहां इंटेल ने LZCNT / TZCNT के लिए गलत डिपो को हटा दिया, उन्होंने "अन-लेमिनेट" इंडेक्सिंग एड्रेसिंग मोड्स, जबकि POPCNT अभी भी किसी भी एड्र मोड को माइक्रो-फ्यूज कर सकते हैं।
अन्य उत्तरों से विचारों / कोड में सुधार:
@ Hidefromkgb के जवाब में एक अच्छा अवलोकन है कि आप एक 3n + 1 के बाद एक सही बदलाव करने में सक्षम होने की गारंटी देते हैं। आप केवल चरणों के बीच चेक को छोड़ने की तुलना में इसे और भी अधिक कुशलता से गणना कर सकते हैं। उस उत्तर में एएसएम कार्यान्वयन टूट गया है, हालांकि (यह OF पर निर्भर करता है, जो SHRD के बाद एक गिनती> 1 के साथ अपरिभाषित है), और धीमा: ROR rdi,2
से तेज है SHRD rdi,rdi,2
, और महत्वपूर्ण पथ पर दो CMOV निर्देशों का उपयोग करना एक अतिरिक्त परीक्षण की तुलना में धीमा है। जो समानांतर में चल सकता है।
मैंने tidied / बेहतर C (जो कि बेहतर asm का उत्पादन करने के लिए संकलक का मार्गदर्शन करता है) को रखा, और Godbolt पर asm (C के नीचे की टिप्पणियों में) तेजी से काम कर रहा है: @ Hidefromkgb के उत्तर में लिंक देखें । (इस उत्तर ने बड़े गॉडबोलेट यूआरएल से 30k चार सीमा को हिट किया, लेकिन शॉर्टलिंक सड़ सकते हैं और वैसे भी goo.gl के लिए बहुत लंबे थे।)
साथ ही write()
एक बार में एक चार लिखने के बजाय एक स्ट्रिंग में बदलने और एक बनाने के लिए आउटपुट-प्रिंटिंग में सुधार हुआ । यह पूरे कार्यक्रम के समय पर प्रभाव को कम करता है perf stat ./collatz
(प्रदर्शन काउंटरों को रिकॉर्ड करने के लिए), और मैंने कुछ गैर-महत्वपूर्ण asm को बाधित किया।
@ वेद्रेक का कोड
मुझे राइट-शिफ्टिंग से एक मामूली स्पीडअप मिला जितना हमें पता है कि क्या करना है, और लूप को जारी रखने के लिए जाँच करना है। 7.5 के लिए सीमा = 1e8 से नीचे 7.275s, Core2Duo (मेरोम) पर, 16 के अनियंत्रित कारक के साथ।
Godbolt पर कोड + टिप्पणियाँ । इस संस्करण का उपयोग क्लैंग के साथ न करें; यह डिफर-लूप के साथ मूर्खतापूर्ण कुछ करता है। एक tmp काउंटर का उपयोग करना k
और फिर count
बाद में इसे जोड़ने से क्लैंग क्या करता है, लेकिन यह थोड़ा दुखता है।
टिप्पणियों में चर्चा देखें: बीएमआई 1 (यानी सेलेरॉन / पेंटियम नहीं) के साथ सीपीयू पर वेडरैक का कोड उत्कृष्ट है