हाथ से लिखे विधानसभा की तुलना में कोलजेट अनुमान का परीक्षण करने के लिए सी ++ कोड - क्यों?


832

मैंने प्रोजेक्ट ईयूलर Q14 के लिए , असेंबली में और C ++ में ये दो समाधान लिखे । Collatz अनुमान का परीक्षण करने के लिए वे समान समान बल बल दृष्टिकोण हैं । विधानसभा समाधान के साथ इकट्ठा किया गया था

nasm -felf64 p14.asm && gcc p14.o -o p14

C ++ के साथ संकलित किया गया था

g++ p14.cpp -o p14

सभा, p14.asm

section .data
    fmt db "%d", 10, 0

global main
extern printf

section .text

main:
    mov rcx, 1000000
    xor rdi, rdi        ; max i
    xor rsi, rsi        ; i

l1:
    dec rcx
    xor r10, r10        ; count
    mov rax, rcx

l2:
    test rax, 1
    jpe even

    mov rbx, 3
    mul rbx
    inc rax
    jmp c1

even:
    mov rbx, 2
    xor rdx, rdx
    div rbx

c1:
    inc r10
    cmp rax, 1
    jne l2

    cmp rdi, r10
    cmovl rdi, r10
    cmovl rsi, rcx

    cmp rcx, 2
    jne l1

    mov rdi, fmt
    xor rax, rax
    call printf
    ret

C ++, p14.cpp

#include <iostream>

using namespace std;

int sequence(long n) {
    int count = 1;
    while (n != 1) {
        if (n % 2 == 0)
            n /= 2;
        else
            n = n*3 + 1;

        ++count;
    }

    return count;
}

int main() {
    int max = 0, maxi;
    for (int i = 999999; i > 0; --i) {
        int s = sequence(i);
        if (s > max) {
            max = s;
            maxi = i;
        }
    }

    cout << maxi << endl;
}

मुझे गति और सब कुछ सुधारने के लिए कंपाइलर ऑप्टिमाइज़ेशन के बारे में पता है, लेकिन मुझे अपने असेंबली सॉल्यूशन को और अधिक ऑप्टिमाइज़ करने के कई तरीके नहीं दिखते हैं (प्रोग्राम को गणितीय रूप से नहीं)।

C ++ कोड में हर पद और हर पद के लिए मापांक होता है, जहां असेंबली प्रति शब्द केवल एक विभाजन है।

लेकिन विधानसभा C ++ समाधान की तुलना में औसतन 1 सेकंड अधिक समय ले रही है। ऐसा क्यों है? मैं मुख्य रूप से जिज्ञासा से बाहर पूछ रहा हूं।

निष्पादन समय

मेरा सिस्टम: 1.4 गीगाहर्ट्ज़ इंटेल सेलेरॉन 2955U (हैसवेल माइक्रोआर्किटेक्चर) पर 64 बिट लिनक्स।


232
क्या आपने असेंबली कोड की जांच की है जो GCC आपके C ++ प्रोग्राम के लिए उत्पन्न करता है?
बर्बाद करें

69
संकलित -Sकरने के लिए संकलित करें कि संकलक ने उत्पन्न किया। संकलक यह समझने के लिए पर्याप्त स्मार्ट है कि मापांक एक ही समय में विभाजन करता है।
user3386109

267
मुझे लगता है कि आपके विकल्प हैं । 1. आपकी मापने की तकनीक त्रुटिपूर्ण है, 2. संकलक बेहतर विधानसभा लिखता है कि आप, या 3. संकलक जादू का उपयोग करता है।
गैलिक


18
@ जफरसन कंपाइलर तेज ब्रूट बल का उपयोग कर सकता है। उदाहरण के लिए शायद SSE निर्देशों के साथ।
user253751

जवाबों:


1896

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

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

संकलक को हाथ से लिखे हुए आसन से पिटाई के बारे में यह सामान्य प्रश्न भी देखें: क्या इनलाइन असेंबली भाषा देशी C ++ कोड से धीमी है? । TL: DR: हाँ अगर आप इसे गलत करते हैं (जैसे यह प्रश्न)।

आमतौर पर आप ठीक कर रहे हैं संकलक अपनी बात करते हैं, खासकर यदि आप C ++ लिखने की कोशिश करते हैं जो कुशलता से संकलित कर सकते हैं । यह भी देखें कि संकलित भाषाओं की तुलना में विधानसभा तेज है? इन सी स्लाइड के उत्तर में से एक यह दिखाता है कि विभिन्न सी कंपाइलर शांत चाल के साथ कुछ बहुत ही सरल कार्यों का अनुकूलन करते हैं। मैट गॉडबोल्ट की CppCon2017 में बात " मेरे साथी ने मेरे लिए क्या किया? संकलक के ढक्कन को खोलना "एक समान नस में है।


even:
    mov rbx, 2
    xor rdx, rdx
    div rbx

इंटेल हैसवेल पर, div r6436 यूओपी है, जिसमें 32-96 चक्रों की विलंबता है , और प्रति 21-74 चक्रों में से एक थ्रूपुट है। (प्लस आरबीएक्स और शून्य आरडीएक्स स्थापित करने के लिए 2 यूओपीएस, लेकिन आउट-ऑफ-ऑर्डर निष्पादन उन लोगों को जल्दी चला सकता है)। DIV जैसे उच्च-यूओपी-गिनती निर्देश माइक्रोकोडेड हैं, जो फ्रंट-एंड अड़चनों का कारण भी बन सकते हैं। इस मामले में, विलंबता सबसे प्रासंगिक कारक है क्योंकि यह एक लूप-आधारित निर्भरता श्रृंखला का हिस्सा है।

shr rax, 1समान अहस्ताक्षरित विभाजन करता है: यह 1 uop है, 1c विलंबता के साथ , और प्रति घड़ी चक्र 2 चला सकता है।

तुलना के लिए, 32-बिट डिवीजन तेज है, लेकिन अभी भी भयानक बनाम बदलाव है। idiv r329 उफ, 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मूल्यों तक पहुंचने तक इंतजार करना है , या क्या बाहर निकलना है और दूसरे क्रम के लिए रजिस्टरों को छूने के बिना, केवल एक शर्त के लिए एक नया प्रारंभ बिंदु प्राप्त करना है। संभवतः यह उपयोगी डेटा पर प्रत्येक श्रृंखला को रखने के लिए सबसे अच्छा है, अन्यथा आपको इसके काउंटर को सशर्त रूप से बढ़ाना होगा।1n


आप शायद 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 (यानी सेलेरॉन / पेंटियम नहीं) के साथ सीपीयू पर वेडरैक का कोड उत्कृष्ट है


4
मैंने थोड़ी देर पहले वेक्टराइज्ड दृष्टिकोण की कोशिश की है, यह मदद नहीं की (क्योंकि आप स्केलर कोड में बहुत बेहतर कर सकते हैं tzcntऔर आप वेक्टर-केस में अपने वेक्टर-तत्वों के बीच सबसे लंबे समय तक चलने वाले अनुक्रम में बंद हैं)।
EOF

3
@EOF: नहीं, मैं भीतरी लूप से बाहर तोड़ने का मतलब जब किसी भी एक वेक्टर तत्व हिट की 1बजाय वे जब सब है (PCMPEQ / PMOVMSK के साथ आसानी से पता लगाने योग्य)। फिर आप PINSRQ और सामान का उपयोग एक तत्व के साथ फिडेल करने के लिए करते हैं जो समाप्त हो गया (और इसके काउंटर), और लूप में वापस कूदें। यह आसानी से एक नुकसान में बदल सकता है, जब आप अक्सर आंतरिक लूप से बाहर निकल रहे होते हैं, लेकिन इसका मतलब है कि आपको हमेशा आंतरिक लूप के हर पुनरावृत्ति के लिए उपयोगी काम के 2 या 4 तत्व मिल रहे हैं। हालांकि संस्मरण के बारे में अच्छी बात है।
पीटर कॉर्डेस

4
@ जफरसन बेस्ट I प्रबंधित Godbolt.org/g/1N70Ib है । मैं उम्मीद कर रहा था कि मैं कुछ और बेहतर कर सकता हूं, लेकिन ऐसा नहीं है।
विड्राक नोव

86
वह बात जो मुझे अविश्वसनीय उत्तरों के बारे में हैरान करती है जैसे कि यह इस तरह के विवरण के लिए दिखाया गया ज्ञान है। मैं उस स्तर तक कभी किसी भाषा या प्रणाली को नहीं जान पाऊंगा और मुझे नहीं पता होगा कि कैसे। शाबाश सर।
कैमडेन_कीड

8
पौराणिक उत्तर !!
सुमित जैन

104

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

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

(नीचे का कोड 32-बिट है, लेकिन आसानी से 64-बिट में परिवर्तित किया जा सकता है)

उदाहरण के लिए, अनुक्रम फ़ंक्शन को केवल 5 निर्देशों के लिए अनुकूलित किया जा सकता है:

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

पूरा कोड इस तरह दिखता है:

include "%lib%/freshlib.inc"
@BinaryType console, compact
options.DebugMode = 1
include "%lib%/freshlib.asm"

start:
        InitializeAll
        mov ecx, 999999
        xor edi, edi        ; max
        xor ebx, ebx        ; max i

    .main_loop:

        xor     esi, esi
        mov     eax, ecx

    .seq:
        inc     esi                 ; counter
        lea     edx, [3*eax+1]      ; edx = 3*n+1
        shr     eax, 1              ; eax = n/2
        cmovc   eax, edx            ; if CF eax = edx
        jnz     .seq                ; jmp if n<>1

        cmp     edi, esi
        cmovb   edi, esi
        cmovb   ebx, ecx

        dec     ecx
        jnz     .main_loop

        OutputValue "Max sequence: ", edi, 10, -1
        OutputValue "Max index: ", ebx, 10, -1

        FinalizeAll
        stdcall TerminateAll, 0

इस कोड को संकलित करने के लिए, FreshLib की आवश्यकता है।

मेरे परीक्षणों में, (1 GHz AMD A4-1200 प्रोसेसर), उपरोक्त कोड प्रश्न से C ++ कोड की तुलना में लगभग चार गुना तेज है (जब संकलित किया गया -O0: 430 एमएस बनाम 1900 एमएस), और दो गुना से अधिक तेजी से (430) एमएस बनाम 830 एमएस) जब सी ++ कोड के साथ संकलित किया जाता है -O3

दोनों कार्यक्रमों का आउटपुट समान है: i = 837799 पर अधिकतम अनुक्रम = 525।


6
हुह, यह चतुर है। SHR ZF को केवल तभी सेट करता है जब EAX 1 (या 0) था। मुझे याद आया कि जब gcc का -O3आउटपुट ऑप्टिमाइज़ कर रहा था, लेकिन मैंने आपके द्वारा किए गए अन्य सभी ऑप्टिमाइज़ेशनों को स्पॉट किया। (लेकिन आप इंक के बजाय काउंटर इंक्रीमेंट के लिए एलईए का उपयोग क्यों करते हैं? यह उस बिंदु पर झंडे को ठीक करने के लिए ठीक है, और शायद पी 4 को छोड़कर किसी भी चीज पर मंदी की ओर बढ़ सकता है (इंक और एसएचआर दोनों के लिए पुराने झंडे पर गलत निर्भरता)। टी कई बंदरगाहों के रूप में चलती है, और अधिक बार महत्वपूर्ण पथ में देरी के कारण संसाधन संघर्ष हो सकता है।)
पीटर कॉर्डेस

4
ओह, वास्तव में बुलडोजर संकलक आउटपुट के साथ थ्रूपुट पर अड़चन कर सकता है। इसमें हसवेल की तुलना में कम विलंबता CMOV और 3-घटक LEA है (जो मैं विचार कर रहा था), इसलिए आपके कोड में लूप-एण्टेड चेन केवल 3 चक्र है। इसमें पूर्णांक रजिस्टरों के लिए शून्य-विलंबता MOV निर्देश भी नहीं है, इसलिए g ++ का बर्बाद MOV निर्देश वास्तव में महत्वपूर्ण पथ की विलंबता को बढ़ाता है, और बुलडोजर के लिए एक बड़ा सौदा है। तो हाँ, हाथ से अनुकूलन वास्तव में सीपीयू के लिए एक महत्वपूर्ण तरीके से संकलक को हरा देता है जो कि बेकार निर्देशों के माध्यम से चबाने के लिए अल्ट्रा-आधुनिक पर्याप्त नहीं हैं।
पीटर कॉर्डेस

95
" C ++ कंपाइलर के बेहतर होने का दावा करना बहुत गलत गलती है। और विशेष रूप से इस मामले में। मानव हमेशा इस कोड को बेहतर बना सकता है और यह विशेष समस्या इस दावे का अच्छा चित्रण है। " आप इसे उल्टा कर सकते हैं और यह उतना ही मान्य होगा। । " मानव का दावा करना बेहतर है, बहुत बुरी गलती है। और विशेष रूप से इस मामले में। मानव हमेशा कोड को बदतर बना सकता है और यह विशेष प्रश्न इस दावे का अच्छा चित्रण है। " तो मुझे नहीं लगता कि आपके पास यहां कोई बिंदु है। , इस तरह के सामान्यीकरण गलत हैं।
15:32 बजे luk32

5
@ luk32 - लेकिन सवाल के लेखक को कोई तर्क नहीं हो सकता है, क्योंकि उनकी विधानसभा भाषा का ज्ञान शून्य के करीब है। मानव बनाम संकलक के बारे में प्रत्येक तर्क, स्पष्ट रूप से मानव को कम से कम कुछ मध्य स्तर के ज्ञान के साथ मानव मान लेता है। अधिक: प्रमेय "मानव लिखित कोड हमेशा बेहतर होगा या संकलक कोड के समान" औपचारिक रूप से सिद्ध होने के लिए बहुत आसान है।
जॉनफाउंड

30
@ luk32: एक कुशल मानव (और आमतौर पर) को कंपाइलर आउटपुट से शुरू करना चाहिए। इसलिए जब तक आप यह सुनिश्चित करने के लिए अपने प्रयासों को बेंचमार्क करते हैं कि वे वास्तव में तेज़ हैं (जिस लक्ष्य हार्डवेयर के लिए आप ट्यूनिंग कर रहे हैं), आप कंपाइलर से भी बदतर नहीं कर सकते। लेकिन हाँ, मुझे सहमत होना होगा कि यह एक मजबूत वक्तव्य है। कंपाइलर आमतौर पर नौसिखिए एएसएम कोडर्स की तुलना में बहुत बेहतर करते हैं। लेकिन यह आमतौर पर एक अनुदेश या दो को बचाने के लिए संभव है जो संकलक के साथ आता है। (हमेशा महत्वपूर्ण मार्ग पर नहीं, हालांकि, यूएआरसी पर निर्भर करता है)। वे जटिल मशीनरी के अत्यधिक उपयोगी टुकड़े हैं, लेकिन वे "स्मार्ट" नहीं हैं।
पीटर कॉर्डेस

24

अधिक प्रदर्शन के लिए: एक साधारण परिवर्तन यह देख रहा है कि n = 3n + 1 के बाद, n भी होगा, इसलिए आप तुरंत 2 से भाग कर सकते हैं। और n 1 नहीं होगा, इसलिए आपको इसके लिए परीक्षण करने की आवश्यकता नहीं है। तो आप कुछ बचा सकते हैं अगर बयान और लिखें:

while (n % 2 == 0) n /= 2;
if (n > 1) for (;;) {
    n = (3*n + 1) / 2;
    if (n % 2 == 0) {
        do n /= 2; while (n % 2 == 0);
        if (n == 1) break;
    }
}

यहां एक बड़ी जीत है: यदि आप n के सबसे कम 8 बिट्स को देखते हैं, तो जब तक आप 2 आठ बार विभाजित नहीं करते तब तक सभी चरण पूरी तरह से आठ बिट्स द्वारा निर्धारित किए जाते हैं। उदाहरण के लिए, यदि अंतिम आठ बिट्स 0x01 हैं, तो यह द्विआधारी में आपकी संख्या है ???? 0000 0001 फिर अगले चरण हैं:

3n+1 -> ???? 0000 0100
/ 2  -> ???? ?000 0010
/ 2  -> ???? ??00 0001
3n+1 -> ???? ??00 0100
/ 2  -> ???? ???0 0010
/ 2  -> ???? ???? 0001
3n+1 -> ???? ???? 0100
/ 2  -> ???? ???? ?010
/ 2  -> ???? ???? ??01
3n+1 -> ???? ???? ??00
/ 2  -> ???? ???? ???0
/ 2  -> ???? ???? ????

तो इन सभी चरणों की भविष्यवाणी की जा सकती है, और 256k + 1 को 81k + 1 के साथ बदल दिया जाता है। सभी संयोजनों के लिए कुछ ऐसा ही होगा। तो आप एक बड़े स्विच स्टेटमेंट के साथ एक लूप बना सकते हैं:

k = n / 256;
m = n % 256;

switch (m) {
    case 0: n = 1 * k + 0; break;
    case 1: n = 81 * k + 1; break; 
    case 2: n = 81 * k + 1; break; 
    ...
    case 155: n = 729 * k + 425; break;
    ...
}

N, 128 तक लूप को चलाएं, क्योंकि उस बिंदु पर n 2 से आठ से कम डिवीजनों के साथ 1 बन सकता है, और एक बार में आठ या अधिक चरणों को करने से आप उस बिंदु को याद कर पाएंगे जहां आप पहली बार 1 तक पहुंचते हैं। फिर "सामान्य" लूप जारी रखें - या एक तालिका तैयार की है जो आपको बताती है कि 1 तक पहुंचने के लिए कितने और चरणों की आवश्यकता है।

पुनश्च। मुझे पक्का संदेह है कि पीटर कॉर्ड्स का सुझाव इसे और भी तेज कर देगा। एक को छोड़कर सभी में कोई सशर्त शाखाएं नहीं होंगी, और जब लूप वास्तव में समाप्त हो जाएगा, तब एक को सही ढंग से भविष्यवाणी की जाएगी। तो कोड कुछ इस तरह होगा

static const unsigned int multipliers [256] = { ... }
static const unsigned int adders [256] = { ... }

while (n > 128) {
    size_t lastBits = n % 256;
    n = (n >> 8) * multipliers [lastBits] + adders [lastBits];
}

व्यवहार में, आप यह मापेंगे कि एक समय में अंतिम 9, 10, 11, 12 बिट्स का प्रसंस्करण तेजी से होगा। प्रत्येक बिट के लिए, तालिका में प्रविष्टियों की संख्या दोगुनी हो जाएगी, और जब टेबल अब L1 कैश में फिट नहीं होती है, तो मैं एक मंदी का सामना करता हूं।

पी पी एस। यदि आपको संचालन की संख्या की आवश्यकता है: प्रत्येक पुनरावृत्ति में हम दो द्वारा ठीक आठ विभाजन करते हैं, और (3n + 1) परिचालनों की एक परिवर्तनीय संख्या होती है, इसलिए परिचालनों की गणना करने का एक स्पष्ट तरीका एक और सरणी होगा। लेकिन हम वास्तव में चरणों की संख्या की गणना कर सकते हैं (लूप की पुनरावृत्तियों की संख्या के आधार पर)।

हम समस्या को थोड़ा पुनर्परिभाषित कर सकते हैं: यदि n (3n + 1) / 2 को विषम से प्रतिस्थापित करें, और n को n / 2 से बदल दें, तो भी। फिर हर पुनरावृत्ति ठीक 8 चरणों में होगी, लेकिन आप इस बात पर विचार कर सकते हैं कि धोखा :-) इसलिए मान लें कि आर ऑपरेशन n <- 3n + 1 और s ऑपरेशन n <- n / 2 हैं। परिणाम काफी हद तक n '= n * 3 ^ r / 2 ^ s होगा, क्योंकि n <- 3n + 1 का अर्थ n <- 3n * (1 + 1 / 3n) है। लघुगणक लेते हुए हम r = (s + log2 (n '/ n)) / log2 (3) पाते हैं।

अगर हम n and 1,000,000 तक लूप करते हैं और एक प्री-कॉम्प्लेक्स टेबल है, तो किसी भी स्टार्ट पॉइंट n point 1,000,000 से कितने पुनरावृत्तियों की आवश्यकता है, तो ऊपर के रूप में r की गणना करते हुए, निकटतम पूर्णांक तक गोल, सही परिणाम देगा जब तक कि वास्तव में बड़ा नहीं होता।


2
या एक स्विच के बजाय गुणा के लिए डेटा लुकअप टेबल बनाएं और स्थिरांक जोड़ें। इंडेक्सिंग दो 256-एंट्री टेबल जंप टेबल की तुलना में तेज है, और कंपाइलर शायद उस परिवर्तन की तलाश नहीं कर रहे हैं।
पीटर कॉर्डेस 11

1
हम्म, मैंने सोचा कि एक मिनट के लिए यह अवलोकन Collatz अनुमान साबित हो सकता है, लेकिन नहीं, बिल्कुल नहीं। हर संभव 8 बिट्स के पीछे, वहाँ चरणों की एक सीमित संख्या है जब तक वे चले गए हैं। लेकिन उनमें से कुछ 8-बिट पैटर्न को पीछे छोड़ते हुए बाकी के बिटस्ट्रिंग को 8 से अधिक कर देंगे, इसलिए यह अनबिके ग्रोथ या रिपीटिंग चक्र को खारिज नहीं कर सकता है।
पीटर कोर्ड्स 11

अपडेट करने के लिए count, आपको तीसरे सरणी की आवश्यकता है, है ना? adders[]आपको यह नहीं बताता कि कितने सही बदलाव किए गए थे।
पीटर कॉर्डेस

बड़ी तालिकाओं के लिए, कैश घनत्व को बढ़ाने के लिए संकरे प्रकारों का उपयोग करना उचित होगा। अधिकांश आर्किटेक्चर पर, एक से एक शून्य-विस्तार लोड uint16_tबहुत सस्ता है। X86 पर, यह 32-बिट से शून्य-विस्तार जैसा ही सस्ता unsigned intहै uint64_t। (MOVZX इंटेल CPUs पर स्मृति से केवल एक लोड बंदरगाह UOP जरूरत है, लेकिन एएमडी सीपीयू के साथ-साथ की क्या ज़रूरत है ALU।) ओह Btw, तुम क्यों प्रयोग कर रहे हैं size_tके लिए lastBits? यह 32-बिट प्रकार के साथ है -m32, और यहां तक ​​कि -mx32(32-बिट पॉइंटर्स के साथ लंबी मोड)। यह निश्चित रूप से गलत प्रकार है n। बस उपयोग करें unsigned
पीटर कॉर्डेस

20

बल्कि असंबंधित नोट पर: अधिक प्रदर्शन हैक!

  • [पहला «अनुमान» आखिरकार @ShreevatsaR द्वारा डिबेक किया गया है; हटा दिया]

  • अनुक्रम का पता लगाने पर, हम वर्तमान तत्व के 2-पड़ोस में केवल 3 संभावित मामले प्राप्त कर सकते हैं N(पहले दिखाया गया है):

    1. [सम विषम]
    2. [विषम सम]
    3. [भी] [भी]

    अतीत को छलांग लगाने के लिए इन 2 तत्वों का मतलब क्रमशः (N >> 1) + N + 1, ((N << 1) + N + 1) >> 1और N >> 2

    Let`s साबित करते हैं कि दोनों मामलों (1) और (2) के लिए पहले सूत्र का उपयोग करना संभव है (N >> 1) + N + 1,।

    मामला (1) स्पष्ट है। केस (2) का तात्पर्य है (N & 1) == 1, इसलिए यदि हम मान लें (सामान्यता की हानि के बिना) कि N 2-बिट लंबा है और इसके बिट्स सबसे अधिक baसे कम से कम-महत्वपूर्ण हैं, तो a = 1, और निम्नलिखित हैं:

    (N << 1) + N + 1:     (N >> 1) + N + 1:
    
            b10                    b1
             b1                     b
           +  1                   + 1
           ----                   ---
           bBb0                   bBb

    जहां B = !b। पहले परिणाम को राइट-शिफ्ट करने से हमें वही मिलता है जो हम चाहते हैं।

    QED: (N & 1) == 1 ⇒ (N >> 1) + N + 1 == ((N << 1) + N + 1) >> 1

    जैसा कि सिद्ध किया गया है, हम एक समय में 2 तत्वों को पार कर सकते हैं, एक एकल टर्नरी ऑपरेशन का उपयोग कर सकते हैं। एक और 2 × समय की कमी।

परिणामस्वरूप एल्गोरिथ्म इस तरह दिखता है:

uint64_t sequence(uint64_t size, uint64_t *path) {
    uint64_t n, i, c, maxi = 0, maxc = 0;

    for (n = i = (size - 1) | 1; i > 2; n = i -= 2) {
        c = 2;
        while ((n = ((n & 3)? (n >> 1) + n + 1 : (n >> 2))) > 2)
            c += 2;
        if (n == 2)
            c++;
        if (c > maxc) {
            maxi = i;
            maxc = c;
        }
    }
    *path = maxc;
    return maxi;
}

int main() {
    uint64_t maxi, maxc;

    maxi = sequence(1000000, &maxc);
    printf("%llu, %llu\n", maxi, maxc);
    return 0;
}

यहां हम तुलना करते हैं n > 2क्योंकि यदि अनुक्रम की कुल लंबाई विषम है तो प्रक्रिया 1 के बजाय 2 पर रुक सकती है।

[संपादित करें:]

Let`s इस का विधानसभा में अनुवाद करें!

MOV RCX, 1000000;



DEC RCX;
AND RCX, -2;
XOR RAX, RAX;
MOV RBX, RAX;

@main:
  XOR RSI, RSI;
  LEA RDI, [RCX + 1];

  @loop:
    ADD RSI, 2;
    LEA RDX, [RDI + RDI*2 + 2];
    SHR RDX, 1;
    SHRD RDI, RDI, 2;    ror rdi,2   would do the same thing
    CMOVL RDI, RDX;      Note that SHRD leaves OF = undefined with count>1, and this doesn't work on all CPUs.
    CMOVS RDI, RDX;
    CMP RDI, 2;
  JA @loop;

  LEA RDX, [RSI + 1];
  CMOVE RSI, RDX;

  CMP RAX, RSI;
  CMOVB RAX, RSI;
  CMOVB RBX, RCX;

  SUB RCX, 2;
JA @main;



MOV RDI, RCX;
ADD RCX, 10;
PUSH RDI;
PUSH RCX;

@itoa:
  XOR RDX, RDX;
  DIV RCX;
  ADD RDX, '0';
  PUSH RDX;
  TEST RAX, RAX;
JNE @itoa;

  PUSH RCX;
  LEA RAX, [RBX + 1];
  TEST RBX, RBX;
  MOV RBX, RDI;
JNE @itoa;

POP RCX;
INC RDI;
MOV RDX, RDI;

@outp:
  MOV RSI, RSP;
  MOV RAX, RDI;
  SYSCALL;
  POP RAX;
  TEST RAX, RAX;
JNE @outp;

LEA RAX, [RDI + 59];
DEC RDI;
SYSCALL;

संकलन करने के लिए इन कमांड का उपयोग करें:

nasm -f elf64 file.asm
ld -o file file.o

Godbolt पर पीटर कॉर्ड्स द्वारा सी और एसम का एक उन्नत / बगिफ़ाइड संस्करण देखें । (संपादक का ध्यान दें: आपके उत्तर में मेरा सामान डालने के लिए क्षमा करें, लेकिन मेरे उत्तर ने गॉडबोल लिंक्स + टेक्स्ट से ३०k चार सीमा को हिट किया है!)


2
ऐसा कोई अभिन्न नहीं Qहै 12 = 3Q + 1। आपका पहला बिंदु सही नहीं है, मेथिंक।
Veedrac

1
@ वीड्राक: इसके साथ खेल रहे हैं: इस उत्तर में कार्यान्वयन की तुलना में बेहतर आरईएम / टेस्ट और केवल एक सीएमओवी का उपयोग करके इसे लागू किया जा सकता है। यह CPU कोड अनंत-छोरों पर मेरे CPU पर, क्योंकि यह जाहिरा तौर पर OF पर निर्भर करता है, जो कि SHRD या ROR के बाद गिनती के साथ अपरिभाषित है mov reg, imm32। 64-बिट संस्करण का रजिस्टर हर जगह, यहां तक ​​कि इसके लिए xor rax, rax, इसमें बहुत सारे अनावश्यक REX उपसर्ग हैं। हमें स्पष्ट रूप से केवल nअतिप्रवाह से बचने के लिए आंतरिक लूप में पकड़े हुए आरईएक्स पर आरईएक्स की आवश्यकता है ।
पीटर कॉर्डेस

1
समय परिणाम (एक Core2Duo E6600 से: मेरोम 2.4GHz। कॉम्प्लेक्स-एलईए = 1 सी विलंबता, CMOV = 2 सी) । सबसे अच्छा एकल-चरण एएसएम इनर-लूप कार्यान्वयन (जॉनफाउंड से): इस @ लूप लूप के प्रति रन 111 मी। इस सी (कुछ tmp vars के साथ) के मेरे de-obfuscated संस्करण से संकलक आउटपुट: clang3.8 -O3 -march=core2: 96ms। gcc5.2: 108ms। क्लैंग के एसम इनर लूप के मेरे उन्नत संस्करण से: 92ms (एसएनबी-परिवार पर एक बहुत बड़ा सुधार देखना चाहिए, जहां जटिल एलईए 3 सी नहीं 1 सी है)। इस asm लूप के मेरे सुधारित + कार्यशील संस्करण से (ROR + TEST, न कि SHRD का उपयोग करके): 87ms। छपाई से पहले 5 प्रतिनिधि के साथ मापा जाता है
पीटर कॉर्डेस

2
यहां पहले 66 रिकॉर्ड-बसे (OEIS पर A006877) हैं; मैंने बोल्ड में भी लोगों को चिह्नित किया है: 2, 3, 6, 7, 9, 18, 25, 27, 54, 73, 97, 129, 171, 231, 313, 327, 649, 703, 871, 1161। 2223, 2463, 2919, 3711, 6171, 10971, 13255, 17647, 23529, 26623, 34239, 35655, 52527, 77031, 106239, 142587, 156159, 216367, 230631, 410011, 511935, 611331, 837331, 83771, 83771। 1723519, 2298025, 3064033, 3542887, 3732423, 5649499, 6649279, 8400511, 11200681, 14934241, 15733191, 31466382, 36791535, 63728127, 127456254, 169941673, 226588897, 268549803, 537099606, 670617279, 1341234558
ShreevatsaR

1
@hidefromkgb बढ़िया! और मैं आपके अन्य बिंदुओं की अब भी बेहतर तरीके से सराहना करता हूं: 4k + 2 → 2k + 1 → 6k + 4 = (4k + 2) + (2k + 1) + 1, और 2k + 1 → 6k + 4 → 3k + 2 = 2k + 1) + (k) + 1. अच्छा अवलोकन!
श्रीवत्सआर

6

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

हालाँकि मेरा मानना ​​है कि आपकी रूपरेखा पद्धति में कुछ दोष हैं। प्रोफाइलिंग के लिए सामान्य दिशानिर्देश निम्नलिखित हैं:

  1. सुनिश्चित करें कि आपका सिस्टम अपनी सामान्य / निष्क्रिय अवस्था में है। उन सभी चल रही प्रक्रियाओं (एप्लिकेशन) को बंद करें जिन्हें आपने शुरू किया था या जो कि गहनता से सीपीयू का उपयोग करते हैं (या नेटवर्क पर सर्वेक्षण)।
  2. आपका डेटा साइज़ बड़ा होना चाहिए।
  3. आपका परीक्षण 5-10 सेकंड से अधिक के लिए चलना चाहिए।
  4. सिर्फ एक नमूने पर भरोसा न करें। अपना टेस्ट N बार करें। परिणाम एकत्र करें और परिणाम के औसत या माध्य की गणना करें।

हां, मैंने कोई औपचारिक रूपरेखा नहीं की है, लेकिन मैंने उन दोनों को कुछ बार चलाया है और 3 सेकंड से 2 सेकंड बताने में सक्षम हूं। वैसे भी जवाब देने के लिए धन्यवाद। मैंने पहले से ही यहाँ जानकारी का एक अच्छा सौदा उठाया है
jeffer बेटा

9
यह शायद केवल माप की त्रुटि नहीं है, हाथ से लिखा हुआ asm कोड राइट-शिफ्ट के बजाय 64-बिट DIV निर्देश का उपयोग कर रहा है। मेरा जवाब देखिए। लेकिन हां, सही तरीके से मापना भी महत्वपूर्ण है।
पीटर कॉर्डेस

7
बुलेट पॉइंट एक कोड ब्लॉक की तुलना में अधिक उपयुक्त स्वरूपण हैं। कृपया अपने पाठ को एक कोड ब्लॉक में डालना बंद करें, क्योंकि यह कोड नहीं है और एक मोनोसैप्ड फ़ॉन्ट से लाभ नहीं करता है।
पीटर कॉर्डेस

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

6

Collatz समस्या के लिए, आप "पूंछ" को कैशिंग करके प्रदर्शन में उल्लेखनीय वृद्धि प्राप्त कर सकते हैं। यह एक समय / स्मृति व्यापार बंद है। देखें: संस्मरण ( https://en.wikipedia.org/wiki/Memoization )। आप अन्य समय / मेमोरी ट्रेड-ऑफ के लिए गतिशील प्रोग्रामिंग समाधानों में भी देख सकते हैं।

उदाहरण अजगर कार्यान्वयन:

import sys

inner_loop = 0

def collatz_sequence(N, cache):
    global inner_loop

    l = [ ]
    stop = False
    n = N

    tails = [ ]

    while not stop:
        inner_loop += 1
        tmp = n
        l.append(n)
        if n <= 1:
            stop = True  
        elif n in cache:
            stop = True
        elif n % 2:
            n = 3*n + 1
        else:
            n = n // 2
        tails.append((tmp, len(l)))

    for key, offset in tails:
        if not key in cache:
            cache[key] = l[offset:]

    return l

def gen_sequence(l, cache):
    for elem in l:
        yield elem
        if elem in cache:
            yield from gen_sequence(cache[elem], cache)
            raise StopIteration

if __name__ == "__main__":
    le_cache = {}

    for n in range(1, 4711, 5):
        l = collatz_sequence(n, le_cache)
        print("{}: {}".format(n, len(list(gen_sequence(l, le_cache)))))

    print("inner_loop = {}".format(inner_loop))

1
gnasher के उत्तर से पता चलता है कि आप पूंछों को कैश करने के बजाय बहुत कुछ कर सकते हैं: उच्च बिट्स आगे जो होता है उसे प्रभावित नहीं करते हैं, और / mul केवल बाईं ओर ले जाने का प्रचार करते हैं, इसलिए उच्च बिट्स कम बिट्स के साथ क्या होता है को प्रभावित नहीं करते हैं। यानी आप एक बार में 8 (या किसी भी संख्या) बिट्स पर जाने के लिए LUT लुकअप का उपयोग कर सकते हैं, बाकी के बिट्स पर लागू करने के लिए गुणा और स्थिरांक जोड़ सकते हैं। इस तरह की कई समस्याओं में, और जब आप बेहतर दृष्टिकोण के बारे में अभी तक नहीं सोचा है, या इसे सही साबित नहीं किया है, तो पूंछ को याद करना निश्चित रूप से बहुत सारी समस्याओं में सहायक है।
पीटर कॉर्डेस

2
अगर मैं gnasher के विचार को सही ढंग से समझता हूं, तो मुझे लगता है कि टेल मेमोनाइजेशन एक ऑर्थोगोनल ऑप्टिमाइज़ेशन है। तो आप गर्भधारण कर सकते हैं दोनों। यह जांचना दिलचस्प होगा कि ज्ञापन के एल्गोरिथ्म में संस्मरण जोड़ने से आपको कितना फायदा हो सकता है।
इमानुएल लैंडहोम

2
हम केवल परिणामों के घने हिस्से को संचय करके सस्ता बना सकते हैं। N और उसके ऊपर एक ऊपरी सीमा निर्धारित करें, स्मृति की जांच भी न करें। उसके नीचे, हैश फ़ंक्शन के रूप में हैश (एन) -> एन का उपयोग करें, इसलिए सरणी में कुंजी = स्थिति, और संग्रहीत करने की आवश्यकता नहीं है। 0साधन की एक प्रविष्टि अभी तक मौजूद नहीं है। हम तालिका में केवल विषम एन को स्टोर करके आगे का अनुकूलन कर सकते हैं, इसलिए हैश फ़ंक्शन है n>>1, 1. त्यागना। चरण कोड को हमेशा n>>tzcnt(n)यह विषम होने के लिए एक या कुछ के साथ समाप्त करने के लिए लिखें ।
पीटर कॉर्डेस

1
यह मेरे (अछूते) विचार पर आधारित है कि एक अनुक्रम के बीच में बहुत बड़े एन मान कई अनुक्रमों के लिए सामान्य होने की संभावना कम है, इसलिए हम उन्हें याद नहीं करने से बहुत अधिक याद नहीं करते हैं। इसके अलावा कि एक यथोचित आकार का N कई लंबे अनुक्रमों का हिस्सा होगा, यहां तक ​​कि बहुत बड़े N से शुरू होता है। (यह इच्छाधारी सोच हो सकती है; यदि यह गलत है तो केवल निरंतर N की घनी श्रेणी को कैशिंग करने से बनाम हैश हो सकता है) तालिका जो मनमानी कुंजी स्टोर कर सकती है।) क्या आपने यह देखने के लिए किसी भी तरह की हिट-रेट टेस्टिंग की है कि क्या पास के एन शुरू करने से उनके अनुक्रम मूल्यों में कोई समानता है?
पीटर कॉर्डेस

2
आप बस कुछ बड़े एन के लिए सभी एन <एन के लिए पूर्व-संगणित परिणामों को स्टोर कर सकते हैं, इसलिए आपको एक हैश तालिका के ओवरहेड की आवश्यकता नहीं है। उस तालिका के डेटा का उपयोग अंततः हर शुरुआती मूल्य के लिए किया जाएगा । यदि आप बस यह पुष्टि करना चाहते हैं कि Collatz अनुक्रम हमेशा (1, 4, 2, 1, 4, 2, ...) में समाप्त होता है: यह n> 1 के लिए यह साबित करने के लिए बराबर साबित हो सकता है, अनुक्रम अंततः होगा मूल n से कम हो। और उस के लिए, कैशिंग पूंछ मदद नहीं करेगा।
gnasher729

5

टिप्पणियों से:

लेकिन, यह कोड कभी नहीं रुकता है (पूर्णांक अतिप्रवाह के कारण)! यवेस डावाड

कई नंबरों के लिए यह अतिप्रवाह नहीं होगा ।

यदि यह अतिप्रवाह करेगा - उन अशुभ प्रारंभिक बीजों में से एक के लिए, अतिप्रवाह संख्या एक और अतिप्रवाह के बिना 1 की ओर अभिसरण होगी।

फिर भी यह दिलचस्प सवाल है, क्या कुछ अतिप्रवाह-चक्रीय बीज संख्या है?

कोई भी सरल अंतिम रूपांतरित श्रृंखला दो मूल्य (स्पष्ट रूप से पर्याप्त?) की शक्ति से शुरू होती है।

2 ^ 64 शून्य पर ओवरफ्लो होगा, जो एल्गोरिथ्म के अनुसार अपरिभाषित अनंत लूप है (केवल 1 के साथ समाप्त होता है), लेकिन उत्तर में सबसे इष्टतम समाधान shr raxZF = 1 के उत्पादन के कारण खत्म हो जाएगा ।

क्या हम 2 ^ 64 का उत्पादन कर सकते हैं? यदि शुरुआती संख्या है 0x5555555555555555, तो यह विषम संख्या है, अगला नंबर 3n + 1 है, जो कि 0xFFFFFFFFFFFFFFFF + 1= है 0। एल्गोरिथम की अपरिभाषित स्थिति में सैद्धांतिक रूप से, लेकिन जॉन्फाउंड का अनुकूलित उत्तर ZF = 1 पर बाहर निकलने से ठीक हो जाएगा। cmp rax,1पीटर Cordes की अनंत लूप में खत्म हो जाएगा (QED संस्करण 1, अपरिभाषित के माध्यम से "cheapo" 0संख्या)।

कैसे कुछ और अधिक जटिल संख्या के बारे में, जो बिना चक्र बनाएगा 0 ? सच कहूँ तो, मुझे यकीन नहीं हो रहा है, मेरा मैथ सिद्धांत किसी भी गंभीर विचार को प्राप्त करने के लिए बहुत जल्दबाजी में है, इसके साथ गंभीर तरीके से कैसे निपटें। लेकिन सहज रूप से मैं कहूंगा कि श्रृंखला प्रत्येक संख्या के लिए 1 में परिवर्तित हो जाएगी: 0 <संख्या, चूंकि 3n + 1 सूत्र धीरे-धीरे मूल संख्या (या मध्यवर्ती) के प्रत्येक गैर -2 प्रमुख कारक को 2, कुछ या बाद की शक्ति में बदल देगा। । इसलिए हमें मूल श्रृंखला के लिए अनंत लूप के बारे में चिंता करने की आवश्यकता नहीं है, केवल अतिप्रवाह हमें बाधित कर सकता है।

इसलिए मैंने बस कुछ नंबरों को शीट में डाल दिया और 8 बिट्स को काट दिया।

वहाँ तीन मानों बह निकला है 0: और 227, ( सीधे जा रहे हैं , अन्य दो प्रगति की ओर1708585085 )।

लेकिन वहाँ कोई चक्रीय अतिप्रवाह बीज बनाने मूल्य है।

पर्याप्त रूप से मैंने एक चेक किया, जो कि 8 बिट ट्रंकेशन से पीड़ित होने वाला पहला नंबर है, और पहले 27से ही प्रभावित है! यह 9232उचित गैर-छंटनी श्रृंखला में मूल्य तक पहुंचता है (पहली छंटनी मूल्य 32212 वें चरण में है), और गैर-छंटनी तरीके से 2-255 इनपुट संख्याओं में से किसी के लिए अधिकतम मूल्य पहुंच गया है 13120( 255स्वयं के लिए), अधिकतम संख्या में कदम के 1बारे में है128 (+ -2, यकीन है कि अगर "1" गिनती करने के लिए नहीं है, आदि ...)।

दिलचस्प रूप से पर्याप्त (मेरे लिए) संख्या 9232कई अन्य स्रोत संख्याओं के लिए अधिकतम है, इसके बारे में क्या खास है? : -O 9232=0x2410 ... हम्मम .. कोई आइडिया नहीं।

दुर्भाग्य से मुझे इस श्रृंखला की कोई गहरी समझ नहीं मिल पा रही है, यह क्यों अभिसिंचित हो जाती है और इन्हें k बिट्स cmp number,1में विभाजित करने के क्या निहितार्थ हैं , लेकिन शर्त को समाप्त करने के साथ निश्चित रूप से एल्गोरिथ्म को अनंत लूप में डालना संभव है, विशेष इनपुट मूल्य के रूप में समाप्त होता है0 बाद काट-छांट।

लेकिन 278 बिट मामले के लिए ओवरफ्लो करने वाला मूल्य अलर्ट करने जैसा है, यह इस तरह दिखता है यदि आप मूल्य तक पहुंचने के लिए चरणों की संख्या की गणना करते हैं 1, तो आपको पूर्णांक के कुल के-बिट सेट से अधिकांश संख्याओं के लिए गलत परिणाम मिलेगा। 8 बिट पूर्णांकों के लिए 256 में से 146 संख्याओं ने छंटनी द्वारा श्रृंखला को प्रभावित किया है (उनमें से कुछ अभी भी दुर्घटना से सही संख्या में कदम उठा सकते हैं, शायद मैं जांच के लिए बहुत आलसी हूं)।


"ओवरफ्लो संख्या बहुत अधिक संभावना होगी 1 बिना किसी अतिप्रवाह के 1": कोड कभी नहीं रुकता। (यह अनुमान है कि जब तक मैं निश्चित होने का इंतजार नहीं कर सकता ...)
यवेस डावा

@YvesDaoust ओह, लेकिन यह करता है? ... उदाहरण के लिए 278b ट्रंकेशन के साथ श्रृंखला इस तरह दिखती है: 82 41 124 62 31 31 47 47 142 71 214 107 66 (काटकर) 33 100 50 25 76 38 19 58 88 88 44 22 11 ३४ १ 34 ५२ २६ १३ ४० २० १० ५ १६ 52 ४ २ १ (बाकी यह बिना छंटनी के काम करता है)। मैं तुम्हें नहीं मिलता, माफ करना। यह कभी नहीं रुकेगा यदि छंटनी का मूल्य पहले से चल रही श्रृंखला में पहले से मौजूद कुछ के बराबर होगा, और मुझे ऐसा कोई मूल्य बनाम k-bit ट्रंकेशन नहीं मिल सकता है (लेकिन मैं या तो गणित के सिद्धांत के पीछे का पता नहीं लगा सकता, क्यों यह 8/16/32/64 बिट्स ट्रंकेशन के लिए है, बस सहजता से मुझे लगता है कि यह काम करता है)।
17

1
मुझे जल्द ही मूल समस्या विवरण की जांच करनी चाहिए थी: "हालांकि यह अभी तक साबित नहीं हुआ है (Collatz Problem), यह माना जाता है कि सभी शुरुआती संख्या 1. पर खत्म होती हैं।" ... ठीक है, कोई आश्चर्य नहीं कि मैं इसे अपने सीमित हाजी मठ ज्ञान के साथ समझ नहीं पा रहा हूं ...: डी और मेरी शीट प्रयोगों से मैं आपको आश्वस्त कर सकता हूं कि यह हर 2- 255संख्या के लिए, या तो बिना ट्रंकेशन ( 1) के लिए अभिसरण करता है ; या 8 बिट ट्रंकेशन (या तो अपेक्षित 1या 0तीन नंबर के लिए) के साथ।
पेड

हेम, जब मैं कहना है कि यह कभी नहीं बंद हो जाता है, मेरा मतलब है ... कि यह बंद नहीं करता है। यदि आप चाहें तो दिए गए कोड हमेशा के लिए चलते हैं।
यवेस डाएव

1
अतिप्रवाह पर क्या होता है, इसके विश्लेषण के लिए तैयार किया गया। CMP- आधारित लूप का उपयोग cmp rax,1 / jna(यानी do{}while(n>1)) शून्य पर भी समाप्त करने के लिए किया जा सकता है। मैंने लूप के एक इंस्ट्रूमेंटेड वर्जन को बनाने के बारे में सोचा n, जो अधिकतम देखा गया रिकॉर्ड करता है, जिससे हम अंदाजा लगा सकते हैं कि हम ओवरफ्लो होने के कितने करीब हैं।
पीटर कॉर्डेस

5

आपने संकलक द्वारा उत्पन्न कोड को पोस्ट नहीं किया है, इसलिए यहां कुछ अनुमान लगाया गया है, लेकिन यहां तक ​​कि इसे देखे बिना भी, कोई भी ऐसा कर सकता है:

test rax, 1
jpe even

... शाखा का गलत उपयोग करने का 50% मौका है, और यह महंगा आ जाएगा।

संकलक लगभग निश्चित रूप से दोनों संगणनाएँ करता है (जो div / mod के बाद से बहुत अधिक खर्च होता है, इसलिए बहु-जोड़ "मुक्त" है) और एक CMOV के साथ अनुसरण करता है। निस्संदेह, जिसके गलत होने का शून्य प्रतिशत संभावना है।


1
ब्रांचिंग के लिए कुछ पैटर्न है; उदाहरण के लिए एक विषम संख्या हमेशा एक सम संख्या के बाद होती है। लेकिन कभी-कभी 3 एन + 1 कई ट्रेलिंग शून्य बिट्स को छोड़ देता है, और जब यह गलत होगा। मैंने अपने उत्तर में विभाजन के बारे में लिखना शुरू किया, और ओपी के कोड में इस दूसरे बड़े लाल झंडे को संबोधित नहीं किया। (यह भी ध्यान दें कि केवल JZ या CMOVZ की तुलना में समता की स्थिति का उपयोग करना वास्तव में अजीब है। यह सीपीयू के लिए भी बदतर है, क्योंकि इंटेल सीपीयू टेस्ट / JZ को मैक्रो-फ्यूज कर सकता है, लेकिन टेस्ट / जेपीई नहीं। एग्नेर फ़्यूज़ का कहना है कि एएमडी फ्यूज़ कर सकता है। किसी भी जेसीसी के साथ परीक्षण / सीएमपी, तो उस स्थिति में यह केवल मानव पाठकों के लिए बदतर है)
पीटर कॉर्ड्स

5

असेंबली को देखे बिना भी, सबसे स्पष्ट कारण यह है कि /= 2संभवतः इसे अनुकूलित किया गया है >>=1और कई प्रोसेसरों में एक बहुत तेज बदलाव ऑपरेशन है। लेकिन भले ही किसी प्रोसेसर में शिफ्ट ऑपरेशन न हो, पूर्णांक विभाजन फ्लोटिंग पॉइंट डिवीजन की तुलना में तेज़ होता है।

संपादित करें: आपके मिलन के ऊपर "पूर्णांक विभाजन फ़्लोटिंग पॉइंट डिवीजन से तेज़ है" कथन के अनुसार भिन्न हो सकता है। नीचे दी गई टिप्पणियों से पता चलता है कि आधुनिक प्रोसेसर ने पूर्णांक विभाजन पर fp डिवीजन को अनुकूलित करने को प्राथमिकता दी है। तो अगर कोई स्पीडअप के सबसे संभावित कारण की तलाश कर रहा था जो इस थ्रेड के सवाल के बारे में पूछता है, तो कंपाइलर ऑप्टिमाइज़िंग के /=2रूप में >>=1देखने के लिए सबसे अच्छा 1 स्थान होगा।


एक असंबंधित नोट पर , यदि nविषम है, तो अभिव्यक्ति n*3+1हमेशा भी होगी। इसलिए जांच की कोई जरूरत नहीं है। आप उस शाखा को बदल सकते हैं

{
   n = (n*3+1) >> 1;
   count += 2;
}

तो पूरा बयान तब होगा

if (n & 1)
{
    n = (n*3 + 1) >> 1;
    count += 2;
}
else
{
    n >>= 1;
    ++count;
}

4
आधुनिक x86 सीपीयू पर एफपीआर डिवीजन की तुलना में इंटेगर डिवीजन वास्तव में तेज नहीं है। मुझे लगता है कि यह इंटेल / एएमडी अपने एफपी डिवाइडर पर अधिक ट्रांजिस्टर खर्च करने के कारण है, क्योंकि यह एक अधिक महत्वपूर्ण ऑपरेशन है। (स्थिरांक द्वारा पूर्णांक विभाजन को एक प्रतिलोम द्वारा एक बहुतायत से अनुकूलित किया जा सकता है)। Agner Fog की insn टेबल की जाँच करें, और DIV r32(32-बिट अहस्ताक्षरित पूर्णांक) या DIV r64(बहुत धीमा 64-बिट अहस्ताक्षरित पूर्णांक) के साथ DIVSD (डबल-सटीक फ्लोट) की तुलना करें । विशेष रूप से थ्रूपुट के लिए, एफपी डिवीजन बहुत तेज है (माइक्रो-कोडेड, और आंशिक रूप से पाइपलाइड के बजाय एकल लूप), लेकिन विलंबता भी बेहतर है।
पीटर कॉर्डेस

1
उदाहरण के लिए ओपी के हसवेल सीपीयू पर: डीआईवीएसडी 1 यूओपी, 10-20 चक्र विलंबता, प्रति 8-14c थ्रूपुट एक है। div r6436 ऊप्स, 32-96 सी लेटेंसी और 21-74 सी प्रति थ्रूपुट में से एक है। Skylake भी तेजी से एफपी डिवीजन थ्रूपुट (4c पर एक बेहतर बेहतर विलंबता के साथ pipelined) है, लेकिन बहुत तेजी से पूर्णांक div नहीं है। एएमडी बुलडोजर-परिवार पर चीजें समान हैं: DIVSD 1M-op, 9-27c विलंबता, एक प्रति 4.5-11c थ्रूपुट है। div r6416M-ops, 16-75c विलंबता, एक प्रति 16-75c थ्रूपुट है।
पीटर कॉर्डेस

1
क्या एफपी डिवीजन मूल रूप से पूर्णांक-घटाव घातांक, पूर्णांक-विभाजित मंटिसा के समान नहीं है, जो विकृतीकरण का पता लगाता है? और उन 3 चरणों को समानांतर में किया जा सकता है।
एमएसलटर्स

2
@MSalters: हाँ, यह सही लगता है, लेकिन घातांक और मंटिस के बीच अंत में ओटी शिफ्ट बिट्स में एक सामान्यीकरण कदम के साथ। doubleएक 53-बिट मंटिसा है, लेकिन यह अभी भी div r32हैसवेल की तुलना में काफी धीमी है । तो यह निश्चित रूप से केवल एक मामला है कि हार्डवेयर इंटेल / एएमडी समस्या में कितना फेंक देते हैं, क्योंकि वे पूर्णांक और एफपी दोनों डिवाइडर के लिए एक ही ट्रांजिस्टर का उपयोग नहीं करते हैं। पूर्णांक एक अदिश राशि है (कोई पूर्णांक-SIMD विभाजन नहीं है), और वेक्टर एक 128b वैक्टर (256 वेक्टर अन्य वेक्टर ALU की तरह नहीं) को संभालता है। बड़ी बात यह है कि पूर्णांक div कई उप्स हैं, आसपास के कोड पर बड़ा प्रभाव।
पीटर कॉर्डेस

एर, मंटिसा और घातांक के बीच बिट्स को शिफ्ट नहीं करते हैं, लेकिन शिफ्ट के साथ मंटिसा को सामान्य करते हैं, और शिफ्ट राशि को घातांक में जोड़ते हैं।
पीटर कॉर्डेस

4

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

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

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


2
इससे पूरी तरह सहमत हैं। gcc -O3बनाया कोड है कि सटीक एल्गोरिथ्म के लिए, Haswell पर इष्टतम के 20% के भीतर था। (उन स्पीडअप को प्राप्त करना मेरे उत्तर का मुख्य केंद्र केवल इसलिए था क्योंकि यही प्रश्न पूछा गया था, और इसका एक दिलचस्प जवाब है, इसलिए नहीं कि सही दृष्टिकोण।) बहुत बड़े स्पीडअप को परिवर्तनों से प्राप्त किया गया था कि संकलक देखने के लिए बेहद संभावना नहीं है। , जैसे कि सही बदलाव, या एक समय में 2 चरण करना। इससे कहीं बड़ा स्पीडअप मेमोसेशन / लुकअप-टेबल से हो सकता है। अभी भी संपूर्ण परीक्षण, लेकिन शुद्ध जानवर बल नहीं।
पीटर कॉर्डेस

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

-2

सरल उत्तर:

  • MOV RBX, 3 और MUL RBX करना महंगा है; सिर्फ ADD RBX, RBX दो बार

  • ADD 1 संभवतः INC से अधिक तेज है

  • MOV 2 और DIV बहुत महंगा है; बस सही बदलाव

  • 64-बिट कोड आमतौर पर 32-बिट कोड की तुलना में काफी धीमा है और संरेखण मुद्दे अधिक जटिल हैं; इस तरह के छोटे कार्यक्रमों के साथ आपको उन्हें पैक करना होगा ताकि आप 32-बिट कोड से अधिक तेज़ होने की किसी भी संभावना के समानांतर गणना कर रहे हों

यदि आप अपने C ++ प्रोग्राम के लिए असेंबली लिस्टिंग जेनरेट करते हैं, तो आप देख सकते हैं कि यह आपके असेंबली से कैसे भिन्न है।


4
1): LEA की तुलना में 3 गुना गूंगा होगा। इसके अलावा mul rbxओपी के हसवेल सीपीयू पर 2 सी है 3 सी विलंबता (और 1 प्रति घड़ी थ्रूपुट) के साथ। imul rcx, rbx, 3केवल 1 uop है, वही 3c विलंबता के साथ। दो ADD निर्देश 2c विलंबता के साथ 2 uops होंगे।
पीटर कॉर्डेस

5
2) ADD 1 यहाँ INC से ज्यादा तेज हैनहींं, ओपी एक पेंटियम 4 का उपयोग नहीं कर रहा है । आपकी बात 3) इस उत्तर का एकमात्र सही हिस्सा है।
पीटर कॉर्डेस

5
4) कुल बकवास की तरह लगता है। 64-बिट कोड सूचक-भारी डेटा संरचनाओं के साथ धीमा हो सकता है, क्योंकि बड़े पॉइंटर्स का मतलब बड़ा कैश फ़ुटप्रिंट है। लेकिन यह कोड केवल रजिस्टरों में काम कर रहा है, और कोड संरेखण मुद्दे 32 और 64 बिट मोड में समान हैं। (तो डेटा संरेखण मुद्दे हैं, कोई सुराग नहीं कि आप किस संरेखण के बारे में x86-64 के लिए एक बड़ा मुद्दा है)। वैसे भी, कोड लूप के अंदर मेमोरी को टच नहीं करता है।
पीटर कॉर्डेस

टिप्पणीकार को कोई पता नहीं है कि किस बारे में बात की जा रही है। 64-बिट सीपीयू पर एक MOV + MUL करें दो बार खुद को रजिस्टर जोड़ने की तुलना में लगभग तीन गुना धीमा होगा। उनकी अन्य टिप्पणियां समान रूप से गलत हैं।
टायलर डर्डन

6
वैसे MOV + MUL निश्चित रूप से गूंगा है, लेकिन MOV + ADD + ADD अभी भी मूर्खतापूर्ण है (वास्तव में ADD RBX, RBXदो बार करना 4 से गुणा करना होगा, 3 नहीं)। अब तक का सबसे अच्छा तरीका है lea rax, [rbx + rbx*2]। या, इसे एक 3-घटक LEA बनाने की कीमत पर, +1 के साथ +1 lea rax, [rbx + rbx*2 + 1] (HSC पर 3c विलंबता, जैसा कि मैंने अपने उत्तर में समझाया है) के साथ करते हैं, मेरा कहना था कि 64-बिट गुणा बहुत महंगा नहीं है हाल ही में इंटेल सीपीयू, क्योंकि उनके पास बहुत तेजी से पूर्णांक गुणक इकाइयां हैं (यहां तक ​​कि एएमडी की तुलना में, जहां MUL r646c विलंबता है, प्रति 4c थ्रूपुट के साथ एक: पूरी तरह से पाइपलाइन भी नहीं है।
पीटर कॉर्डेस
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.