64-बिट के साथ 32-बिट लूप काउंटर को बदलना इंटेल सीपीयू पर _mm_popcnt_u64 के साथ पागल प्रदर्शन विचलन का परिचय देता है


1424

मैं popcountडेटा के बड़े सरणियों का सबसे तेज़ तरीका ढूंढ रहा था । मुझे एक बहुत ही अजीब प्रभाव का सामना करना पड़ा : मेरे पीसी पर प्रदर्शन ड्रॉप को 50% unsignedतक uint64_tबनाने के लिए लूप चर को बदलना ।

बेंचमार्क

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

    using namespace std;
    if (argc != 2) {
       cerr << "usage: array_size in MB" << endl;
       return -1;
    }

    uint64_t size = atol(argv[1])<<20;
    uint64_t* buffer = new uint64_t[size/8];
    char* charbuffer = reinterpret_cast<char*>(buffer);
    for (unsigned i=0; i<size; ++i)
        charbuffer[i] = rand()%256;

    uint64_t count,duration;
    chrono::time_point<chrono::system_clock> startP,endP;
    {
        startP = chrono::system_clock::now();
        count = 0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with unsigned
            for (unsigned i=0; i<size/8; i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "unsigned\t" << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }
    {
        startP = chrono::system_clock::now();
        count=0;
        for( unsigned k = 0; k < 10000; k++){
            // Tight unrolled loop with uint64_t
            for (uint64_t i=0;i<size/8;i+=4) {
                count += _mm_popcnt_u64(buffer[i]);
                count += _mm_popcnt_u64(buffer[i+1]);
                count += _mm_popcnt_u64(buffer[i+2]);
                count += _mm_popcnt_u64(buffer[i+3]);
            }
        }
        endP = chrono::system_clock::now();
        duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
        cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
             << (10000.0*size)/(duration) << " GB/s" << endl;
    }

    free(charbuffer);
}

जैसा कि आप देखते हैं, हम यादृच्छिक डेटा का एक बफर बनाते हैं, जिसका आकार xमेगाबाइट है जहां xकमांड लाइन से पढ़ा जाता है। बाद में, हम बफर पर पुनरावृति popcountकरते हैं और पॉपकाउंट करने के लिए x86 आंतरिक के अनियंत्रित संस्करण का उपयोग करते हैं। अधिक सटीक परिणाम प्राप्त करने के लिए, हम 10,000 बार पॉपकाउंट करते हैं। हम पॉपकाउंट के लिए समय को मापते हैं। ऊपरी मामले में, आंतरिक लूप चर है unsigned, निचले मामले में, आंतरिक लूप चर हैuint64_t । मैंने सोचा था कि इससे कोई फर्क नहीं पड़ना चाहिए, लेकिन मामला इससे उलट है।

(बिल्कुल पागल) परिणाम

मैं इसे इस तरह संकलित करता हूं (g ++ संस्करण: Ubuntu 4.8.2-19ubuntu1):

g++ -O3 -march=native -std=c++11 test.cpp -o test

यहां मेरे हैसवेल कोर i7-4770K CPU @ 3.50 GHz पर परिणाम चल रहे हैं, test 1(इसलिए 1 एमबी रैंडम डेटा):

  • अहस्ताक्षरित 41959360000 0.401554 सेकंड 26.113 GB / s
  • uint64_t 41959360000 0.759822 सेकेंड 13.8003 GB / s

जैसा कि आप देखते हैं, uint64_tसंस्करण का थ्रूपुट केवल संस्करण का आधा हिस्सा है unsigned! समस्या यह प्रतीत होती है कि अलग-अलग विधानसभा उत्पन्न होती है, लेकिन क्यों? सबसे पहले, मैं एक संकलक बग के बारे में सोचा है, इसलिए मैं करने की कोशिश की clang++(उबंटू बजना संस्करण 3.4-1ubuntu3):

clang++ -O3 -march=native -std=c++11 teest.cpp -o test

परिणाम: test 1

  • अहस्ताक्षरित 41959360000 0.398293 सेकंड 26.3267 GB / s
  • uint64_t 41959360000 0.680954 सेकंड 15.3986 GB / s

तो, यह लगभग एक ही परिणाम है और अभी भी अजीब है। लेकिन अब यह सुपर अजीब हो जाता है। मैं एक निरंतरता के साथ इनपुट से पढ़े गए बफर आकार को प्रतिस्थापित करता हूं 1, इसलिए मैं बदलता हूं:

uint64_t size = atol(argv[1]) << 20;

सेवा

uint64_t size = 1 << 20;

इस प्रकार, संकलक अब संकलन समय पर बफर आकार जानता है। शायद यह कुछ अनुकूलन जोड़ सकता है! इसके लिए नंबर इस प्रकार हैं g++:

  • अहस्ताक्षरित 41959360000 0.509156 सेकंड 20.5944 GB / s
  • uint64_t 41959360000 0.508673 सेकंड 20.6139 GB / s

अब, दोनों संस्करण समान रूप से तेज़ हैं। हालांकि, unsigned भी धीमी हो गई ! यह से हटा दिया 26करने के लिए 20 GB/s, इस प्रकार एक करने के लिए एक स्थिर मान नेतृत्व द्वारा एक गैर निरंतर जगह deoptimization । गंभीरता से, मुझे कोई सुराग नहीं है कि यहाँ क्या हो रहा है! लेकिन अब clang++नए संस्करण के साथ:

  • अहस्ताक्षरित 41959360000 0.677009 सेकंड 15.4884 GB / s
  • uint64_t 41959360000 0.676909 सेकंड 15.4906 GB / s

रुको क्या? अब, दोनों संस्करण 15 जीबी / एस की धीमी संख्या में गिर गए । इस प्रकार, एक स्थिर मान द्वारा गैर-स्थिरांक को बदलने पर भी दोनों में धीमा कोड होता है क्लैंग के लिए मामलों !

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

अधिक पागलपन, कृपया!

पहला उदाहरण लें (एक के साथ atol(argv[1])) और staticचर से पहले एक , यानी:

static uint64_t size=atol(argv[1])<<20;

यहाँ जी ++ में मेरे परिणाम हैं:

  • अहस्ताक्षरित 41959360000 0.396728 सेकंड 26.4306 GB / s
  • uint64_t 41959360000 0.509484 सेकंड 20.5811 GB / s

हां, फिर भी एक और विकल्प । हमारे पास अभी भी तेज 26 जीबी / एस है u32, लेकिन हम u64कम से कम 13 जीबी / एस से 20 जीबी / एस संस्करण तक पहुंचने में कामयाब रहे ! मेरे सहयोगी के पीसी पर, u64संस्करण सभी की तुलना u32में सबसे तेज परिणाम प्राप्त करते हुए, संस्करण से भी तेज हो गया । अफसोस की बात है, यह केवल के लिए काम करता है g++, के clang++बारे में परवाह नहीं हैstatic

मेरा प्रश्न

क्या आप इन परिणामों की व्याख्या कर सकते हैं? विशेष रूप से:

  • कैसे u32और के बीच इतना अंतर हो सकता है u64?
  • एक निरंतर बफर आकार द्वारा एक गैर-स्थिरांक को कम इष्टतम कोड कैसे बदला जा सकता है ?
  • staticकीवर्ड का सम्मिलन u64लूप को कैसे तेज बना सकता है? मेरे कॉलगर्ल के कंप्यूटर पर मूल कोड से भी तेज!

मुझे पता है कि अनुकूलन एक मुश्किल क्षेत्र है, हालांकि, मैंने कभी नहीं सोचा था कि इस तरह के छोटे बदलाव 100% अंतर पैदा कर सकते हैं से निष्पादन समय में हो सकता है और यह कि एक निरंतर बफर आकार जैसे छोटे कारक फिर से पूरी तरह से परिणाम मिला सकते हैं। बेशक, मैं हमेशा वह संस्करण चाहता हूं जो 26 जीबी / एस को पॉपकॉर्न करने में सक्षम हो। एकमात्र विश्वसनीय तरीका जो मैं सोच सकता हूं कि इस मामले के लिए विधानसभा को कॉपी पेस्ट करें और इनलाइन विधानसभा का उपयोग करें। यह एकमात्र तरीका है जिससे मैं उन कंपाइलरों से छुटकारा पा सकता हूं जो छोटे परिवर्तनों पर पागल होने लगते हैं। तुम क्या सोचते हो? क्या अधिकांश प्रदर्शन के साथ कोड को मज़बूती से प्राप्त करने का एक और तरीका है?

द डिस्सैड

यहाँ विभिन्न परिणामों के लिए disassembly है:

G ++ / u32 / non-const bufsize से 26 GB / s संस्करण :

0x400af8:
lea 0x1(%rdx),%eax
popcnt (%rbx,%rax,8),%r9
lea 0x2(%rdx),%edi
popcnt (%rbx,%rcx,8),%rax
lea 0x3(%rdx),%esi
add %r9,%rax
popcnt (%rbx,%rdi,8),%rcx
add $0x4,%edx
add %rcx,%rax
popcnt (%rbx,%rsi,8),%rcx
add %rcx,%rax
mov %edx,%ecx
add %rax,%r14
cmp %rbp,%rcx
jb 0x400af8

G ++ / u64 / non-const bufsize से 13 GB / s संस्करण :

0x400c00:
popcnt 0x8(%rbx,%rdx,8),%rcx
popcnt (%rbx,%rdx,8),%rax
add %rcx,%rax
popcnt 0x10(%rbx,%rdx,8),%rcx
add %rcx,%rax
popcnt 0x18(%rbx,%rdx,8),%rcx
add $0x4,%rdx
add %rcx,%rax
add %rax,%r12
cmp %rbp,%rdx
jb 0x400c00

Clang ++ / u64 / non-const bufsize से 15 GB / s संस्करण :

0x400e50:
popcnt (%r15,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r15,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r15,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r15,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp %rbp,%rcx
jb 0x400e50

G ++ / u32 और u64 / const bufsize से 20 GB / s संस्करण :

0x400a68:
popcnt (%rbx,%rdx,1),%rax
popcnt 0x8(%rbx,%rdx,1),%rcx
add %rax,%rcx
popcnt 0x10(%rbx,%rdx,1),%rax
add %rax,%rcx
popcnt 0x18(%rbx,%rdx,1),%rsi
add $0x20,%rdx
add %rsi,%rcx
add %rcx,%rbp
cmp $0x100000,%rdx
jne 0x400a68

Clang ++ / u32 और u64 / const bufsize से 15 GB / s संस्करण :

0x400dd0:
popcnt (%r14,%rcx,8),%rdx
add %rbx,%rdx
popcnt 0x8(%r14,%rcx,8),%rsi
add %rdx,%rsi
popcnt 0x10(%r14,%rcx,8),%rdx
add %rsi,%rdx
popcnt 0x18(%r14,%rcx,8),%rbx
add %rdx,%rbx
add $0x4,%rcx
cmp $0x20000,%rcx
jb 0x400dd0

दिलचस्प है, सबसे तेज़ (26 जीबी / एस) संस्करण भी सबसे लंबा है! यह एकमात्र समाधान लगता है जो उपयोग करता है lea। कुछ संस्करण jbकूदने के लिए उपयोग करते हैं, अन्य उपयोग करते हैं jne। लेकिन इसके अलावा, सभी संस्करण तुलनीय प्रतीत होते हैं। मुझे नहीं लगता कि 100% प्रदर्शन अंतर कहां से उत्पन्न हो सकता है, लेकिन मैं विधानसभा में निपुण नहीं हूं। सबसे धीमा (13 GB / s) संस्करण बहुत छोटा और अच्छा लग रहा है। क्या कोई इसे समझा सकता है?

सीख सीखी

कोई फर्क नहीं पड़ता कि इस सवाल का जवाब क्या होगा; मैंने सीखा है कि वास्तव में गर्म छोरों में हर विस्तार से कोई फर्क पड़ सकता है, यहां तक ​​कि यह विवरण भी कि गर्म कोड के लिए कोई संबंध नहीं है । मैंने कभी नहीं सोचा है कि लूप वेरिएबल के लिए किस प्रकार का उपयोग करना है, लेकिन जैसा कि आप देखते हैं कि ऐसा मामूली बदलाव 100% अंतर ला सकता है! यहां तक ​​कि बफर का भंडारण प्रकार भी बहुत बड़ा अंतर बना सकता है, जैसा कि हमने staticआकार चर के सामने कीवर्ड के सम्मिलन के साथ देखा था ! भविष्य में, मैं हमेशा विभिन्न कंपाइलरों पर विभिन्न विकल्पों का परीक्षण करूंगा जब वास्तव में तंग और गर्म छोरों को लिखना होगा जो सिस्टम प्रदर्शन के लिए महत्वपूर्ण हैं।

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


8
बहुत धन्यवाद टिप्पणियाँ! आप उन्हें चैट में देख सकते हैं और यहां तक ​​कि अगर आप चाहें तो अपना खुद का वहां छोड़ सकते हैं, लेकिन कृपया यहां और न जोड़ें!
शोग

3
इसके अलावा जीसीसी इश्यू 62011, पॉपकंट इंस्ट्रक्शन में गलत डेटा डिपेंडेंसी देखें । किसी और ने इसे प्रदान किया, लेकिन ऐसा लगता है कि सफाई के दौरान खो गया है।
jww

मैं नहीं बता सकता लेकिन स्थैतिक के साथ संस्करण के लिए disassemblies में से एक है? यदि नहीं, तो क्या आप पोस्ट को संपादित कर उसे जोड़ सकते हैं?
केली एस। फ्रेंच

जवाबों:


1552

Culprit: गलत डेटा निर्भरता (और संकलक को भी इसकी जानकारी नहीं है)

सैंडी / आइवी ब्रिज और हैसवेल प्रोसेसर पर, निर्देश:

popcnt  src, dest

गंतव्य रजिस्टर पर एक झूठी निर्भरता दिखाई देती है dest। भले ही निर्देश केवल इसे लिखता है, लेकिन destनिष्पादन से पहले तैयार होने तक निर्देश इंतजार करेगा । यह गलत निर्भरता है (अब) Intel द्वारा इरेटा को HSD146 (Haswell) और SKL029 (Skylake) के रूप में प्रलेखित

Skylake के लिए इस तय lzcntऔरtzcnt
तोप झील (और बर्फ झील) के लिए यह तय किया popcnt
bsf/ bsrएक सच्चे उत्पादन निर्भरता है: इनपुट के लिए unmodified आउटपुट = 0। (लेकिन आंतरिक रूप से इसका लाभ उठाने का कोई तरीका नहीं है - केवल एएमडी दस्तावेज यह और संकलक इसे उजागर नहीं करते हैं।)

(हां, ये निर्देश सभी एक ही निष्पादन इकाई पर चलते हैं )।


यह निर्भरता सिर्फ 4 को नहीं रखती है popcnt एक लूप पुनरावृत्ति से एस तक । यह लूप पुनरावृत्तियों को पार कर सकता है जिससे प्रोसेसर के लिए विभिन्न लूप पुनरावृत्तियों को समानांतर करना असंभव हो जाता है।

unsignedबनाम uint64_tऔर अन्य तोड़ मरोड़ सीधे समस्या को प्रभावित नहीं करते। लेकिन वे रजिस्टर आवंटनकर्ता को प्रभावित करते हैं जो रजिस्टरों को चर को सौंपता है।

आपके मामले में, गति का एक सीधा परिणाम यह है कि रजिस्टर आवंटनकर्ता ने जो करने का फैसला किया है, उसके आधार पर (झूठी) निर्भरता श्रृंखला में फंस गया है।

  • 13 जीबी / एस में एक श्रृंखला है: popcnt- add- popcnt- popcnt→ अगले पुनरावृत्ति
  • 15 जीबी / एस में एक श्रृंखला है: popcnt- add- popcnt- add→ अगले पुनरावृत्ति
  • 20 जीबी / एस में एक श्रृंखला है: popcnt- popcnt→ अगला पुनरावृति
  • 26 जीबी / एस के पास एक श्रृंखला है: popcnt- popcnt→ अगला पुनरावृति

20 जीबी / एस और 26 जीबी / एस के बीच का अंतर अप्रत्यक्ष रूप से संबोधित करने की एक छोटी सी कलाकृति प्रतीत होता है। किसी भी तरह से, प्रोसेसर इस गति तक पहुँचने के बाद अन्य अड़चनों को मारने लगता है।


इसका परीक्षण करने के लिए, मैंने संकलक को बायपास करने के लिए इनलाइन असेंबली का उपयोग किया और मुझे जो असेम्बली चाहिए, वह बिल्कुल मिल गई। मैं countअन्य सभी निर्भरताओं को तोड़ने के लिए चर को विभाजित करता हूं जो बेंचमार्क के साथ गड़बड़ कर सकते हैं।

यहाँ परिणाम हैं:

सैंडी ब्रिज Xeon @ 3.5 GHz: (पूर्ण परीक्षण कोड नीचे पाया जा सकता है)

  • GCC 4.6.3: g++ popcnt.cpp -std=c++0x -O3 -save-temps -march=native
  • उबंटू 12

विभिन्न रजिस्टर: 18.6195 जीबी / एस

.L4:
    movq    (%rbx,%rax,8), %r8
    movq    8(%rbx,%rax,8), %r9
    movq    16(%rbx,%rax,8), %r10
    movq    24(%rbx,%rax,8), %r11
    addq    $4, %rax

    popcnt %r8, %r8
    add    %r8, %rdx
    popcnt %r9, %r9
    add    %r9, %rcx
    popcnt %r10, %r10
    add    %r10, %rdi
    popcnt %r11, %r11
    add    %r11, %rsi

    cmpq    $131072, %rax
    jne .L4

वही रजिस्टर: 8.49272 जीबी / एस

.L9:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # This time reuse "rax" for all the popcnts.
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L9

टूटी श्रृंखला के साथ एक ही रजिस्टर: 17.8869 जीबी / एस

.L14:
    movq    (%rbx,%rdx,8), %r9
    movq    8(%rbx,%rdx,8), %r10
    movq    16(%rbx,%rdx,8), %r11
    movq    24(%rbx,%rdx,8), %rbp
    addq    $4, %rdx

    # Reuse "rax" for all the popcnts.
    xor    %rax, %rax    # Break the cross-iteration dependency by zeroing "rax".
    popcnt %r9, %rax
    add    %rax, %rcx
    popcnt %r10, %rax
    add    %rax, %rsi
    popcnt %r11, %rax
    add    %rax, %r8
    popcnt %rbp, %rax
    add    %rax, %rdi

    cmpq    $131072, %rdx
    jne .L14

तो संकलक के साथ क्या गलत हुआ?

ऐसा लगता है कि न तो जीसीसी और न ही विजुअल स्टूडियो को इसकी जानकारी है popcnt इस तरह की झूठी निर्भरता है। फिर भी, ये गलत निर्भरताएं असामान्य नहीं हैं। यह सिर्फ एक बात है कि क्या संकलक को इसके बारे में पता है।

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

( अपडेट: 4.9.2 संस्करण के रूप में , जीसीसी इस झूठी-निर्भरता से अवगत है और अनुकूलन सक्षम होने पर इसकी भरपाई करने के लिए कोड उत्पन्न करता है। क्लैंग, एमएसवीसी, और यहां तक ​​कि इंटेल के अपने आईसीसी सहित अन्य विक्रेताओं के प्रमुख संकलक अभी तक जागरूक नहीं हैं। यह माइक्रोआर्किटेक्चरल इरेटम और इसके लिए क्षतिपूर्ति करने वाले कोड का उत्सर्जन नहीं करेगा।)

सीपीयू के पास ऐसी झूठी निर्भरता क्यों है?

हम अनुमान लगा सकते हैं: यह एक ही निष्पादन इकाई पर चलता है bsf/ bsrजिसके पास आउटपुट निर्भरता है। ( हार्डवेयर में POPCNT कैसे लागू किया जाता है? )। उन निर्देशों के लिए, इंटेल इनपुट = 0 के लिए पूर्णांक परिणाम को "अपरिभाषित" (ZF = 1 के साथ) के रूप में प्रस्तुत करता है, लेकिन इंटेल हार्डवेयर वास्तव में पुराने सॉफ्टवेयर को तोड़ने से बचने के लिए एक मजबूत गारंटी देता है: आउटपुट अनमॉडिफाइड। AMD इस व्यवहार का दस्तावेज़।

संभवतः इस निष्पादन इकाई के आउटपुट के लिए कुछ यूओपी बनाने के लिए किसी तरह असुविधाजनक था, लेकिन अन्य नहीं।

AMD प्रोसेसर के पास यह गलत निर्भरता नहीं है।


पूर्ण परीक्षण कोड संदर्भ के लिए नीचे है:

#include <iostream>
#include <chrono>
#include <x86intrin.h>

int main(int argc, char* argv[]) {

   using namespace std;
   uint64_t size=1<<20;

   uint64_t* buffer = new uint64_t[size/8];
   char* charbuffer=reinterpret_cast<char*>(buffer);
   for (unsigned i=0;i<size;++i) charbuffer[i]=rand()%256;

   uint64_t count,duration;
   chrono::time_point<chrono::system_clock> startP,endP;
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %4  \n\t"
                "add %4, %0     \n\t"
                "popcnt %5, %5  \n\t"
                "add %5, %1     \n\t"
                "popcnt %6, %6  \n\t"
                "add %6, %2     \n\t"
                "popcnt %7, %7  \n\t"
                "add %7, %3     \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "No Chain\t" << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Chain 4   \t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }
   {
      uint64_t c0 = 0;
      uint64_t c1 = 0;
      uint64_t c2 = 0;
      uint64_t c3 = 0;
      startP = chrono::system_clock::now();
      for( unsigned k = 0; k < 10000; k++){
         for (uint64_t i=0;i<size/8;i+=4) {
            uint64_t r0 = buffer[i + 0];
            uint64_t r1 = buffer[i + 1];
            uint64_t r2 = buffer[i + 2];
            uint64_t r3 = buffer[i + 3];
            __asm__(
                "xor %%rax, %%rax   \n\t"   // <--- Break the chain.
                "popcnt %4, %%rax   \n\t"
                "add %%rax, %0      \n\t"
                "popcnt %5, %%rax   \n\t"
                "add %%rax, %1      \n\t"
                "popcnt %6, %%rax   \n\t"
                "add %%rax, %2      \n\t"
                "popcnt %7, %%rax   \n\t"
                "add %%rax, %3      \n\t"
                : "+r" (c0), "+r" (c1), "+r" (c2), "+r" (c3)
                : "r"  (r0), "r"  (r1), "r"  (r2), "r"  (r3)
                : "rax"
            );
         }
      }
      count = c0 + c1 + c2 + c3;
      endP = chrono::system_clock::now();
      duration=chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "Broken Chain\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
            << (10000.0*size)/(duration) << " GB/s" << endl;
   }

   free(charbuffer);
}

समान रूप से दिलचस्प बेंचमार्क यहां पाया जा सकता है: http://pastebin.com/kbzgL8si
यह बेंचमार्क उन popcnts की संख्या को बदलता है जो (झूठी) निर्भरता श्रृंखला में हैं।

False Chain 0:  41959360000 0.57748 sec     18.1578 GB/s
False Chain 1:  41959360000 0.585398 sec    17.9122 GB/s
False Chain 2:  41959360000 0.645483 sec    16.2448 GB/s
False Chain 3:  41959360000 0.929718 sec    11.2784 GB/s
False Chain 4:  41959360000 1.23572 sec     8.48557 GB/s

3
हैलो यारो! यहाँ पिछले टिप्पणियों के बहुत सारे; एक नया छोड़ने से पहले, पुरालेख की समीक्षा करें
शोग

1
@ जस्टिनलिट ऐसा लगता है कि इस विशेष मुद्दे को क्लैंग में 7.0 के रूप में तय किया गया है
दान एम।

@PeterCordes मुझे नहीं लगता कि यह निष्पादन इकाई है जितना कि यह अनुसूचक है। यह शेड्यूलर है जो निर्भरता को ट्रैक करता है। और ऐसा करने के लिए, निर्देशों को कई "निर्देश वर्गों" में वर्गीकृत किया जाता है, जिनमें से प्रत्येक को अनुसूचक द्वारा पहचाना जाता है। इस प्रकार सभी 3-चक्र "स्लो-इंट" निर्देशों को निर्देशन के उद्देश्य से उसी "क्लास" में फेंक दिया गया।
मि।

@ मिस्टिक: आप अभी भी ऐसा सोचते हैं? यह प्रशंसनीय है, लेकिन imul dst, src, immआउटपुट निर्भरता नहीं है, और न ही धीमा है- lea। न तो करता है pdep, लेकिन यह 2 इनपुट ऑपरेंड के साथ VEX एनकोडेड है। सहमत है कि यह निष्पादन इकाई ही नहीं है जो झूठी dep का कारण बनता है; यह RAT और समस्या / नाम बदलने के चरण तक है क्योंकि यह भौतिक रजिस्टरों पर आर्किटेक्चरल रजिस्टर ऑपरेंड का नाम बदल देता है। संभवतः इसे यूओपी-कोड -> निर्भरता पैटर्न और पोर्ट विकल्पों की तालिका की आवश्यकता है, और एक ही निष्पादन इकाई के लिए सभी यूओपी को एक साथ समूहित करना उस तालिका को सरल बनाता है। यही मेरा मतलब है और अधिक विस्तार से।
पीटर कॉर्डेस

मुझे बताएं कि क्या आप चाहते हैं कि मैं इसे आपके उत्तर में संपादित करूं, या यदि आप इसे वापस कुछ कहना चाहते हैं जैसे कि आपने मूल रूप से अनुसूचक के बारे में क्या कहा था। तथ्य यह है कि SKL ने lzcnt / tzcnt के लिए गलत dep गिरा दिया, लेकिन popcnt को हमें कुछ नहीं बताना चाहिए, लेकिन IDR क्या है। एक अन्य संभावित संकेत है कि यह नाम / RAT से संबंधित है कि SKL एक अनुक्रमित पता मोड को lzcnt / tzcnt के लिए मेमोरी स्रोत के रूप में अनलॉकेट करता है, लेकिन पॉपकंट नहीं। जाहिर है कि नाम बदलने की इकाई को उफ बनाना है क्योंकि बैक-एंड प्रतिनिधित्व कर सकता है, हालांकि।
पीटर कॉर्डेस

50

मैंने प्रयोग करने के लिए एक बराबर सी कार्यक्रम को कोडित किया है, और मैं इस अजीब व्यवहार की पुष्टि कर सकता हूं। क्या अधिक है, 64-बिट uint का उपयोग करने के लिए gcc का उपयोग करने के कारणों के रूप में, gcc64-बिट पूर्णांक (जो शायद size_tवैसे भी होना चाहिए ...) को बेहतर मानता है uint_fast32_t

मैंने असेंबली के साथ थोड़ी बहुत छेड़छाड़ की:
बस 32-बिट संस्करण लें, सभी 32-बिट निर्देशों / रजिस्टरों को 64-बिट संस्करण के साथ प्रोग्राम के इनर पॉपकाउंट-लूप में बदलें। अवलोकन: कोड 32-बिट संस्करण के समान ही तेज़ है!

यह स्पष्ट रूप से एक हैक है, क्योंकि चर का आकार वास्तव में 64 बिट नहीं है, क्योंकि कार्यक्रम के अन्य भाग अभी भी 32-बिट संस्करण का उपयोग करते हैं, लेकिन जब तक आंतरिक पॉपकाउंट-लूप प्रदर्शन पर हावी रहता है, यह एक अच्छी शुरुआत है ।

मैंने तब कार्यक्रम के 32-बिट संस्करण से आंतरिक लूप कोड की प्रतिलिपि बनाई, इसे 64 बिट तक हैक किया, इसे 64-बिट संस्करण के आंतरिक लूप के लिए प्रतिस्थापन बनाने के लिए रजिस्टरों के साथ fiddled किया। यह कोड 32-बिट संस्करण के रूप में भी तेजी से चलता है।

मेरा निष्कर्ष यह है कि यह कंपाइलर द्वारा खराब इंस्ट्रक्शन शेड्यूलिंग है, न कि 32-बिट निर्देशों का वास्तविक गति / विलंबता लाभ।

(कैविएट: मैंने असेंबली को हैक कर लिया था, बिना सूचना के कुछ तोड़ सकता था। मुझे ऐसा नहीं लगता।


1
"क्या अधिक है, जीसीसी 64-बिट पूर्णांक […] को बेहतर मानता है, क्योंकि uint_fast32_t का उपयोग करने से जीसीसी 64-बिट यूइंट का उपयोग करने का कारण बनता है।" दुर्भाग्य से, और मेरे अफसोस के लिए, इन प्रकारों के पीछे कोई जादू और कोई गहरा कोड आत्मनिरीक्षण नहीं है। मैंने अभी तक उन्हें पूरे मंच पर हर संभव जगह और हर कार्यक्रम के लिए एकल टाइपिडफ के अलावा कोई अन्य तरीका प्रदान नहीं किया है। प्रकारों की सटीक पसंद के पीछे बहुत कुछ सोचा जाने की संभावना है, लेकिन उनमें से प्रत्येक के लिए एक परिभाषा संभवतः हर आवेदन के लिए फिट नहीं हो सकती है जो कभी भी होगी। कुछ आगे पढ़ने: stackoverflow.com/q/4116297
केनो

2
@ केनो ऐसा इसलिए sizeof(uint_fast32_t)है क्योंकि इसे परिभाषित किया जाना है। यदि आप इसे नहीं होने देते हैं, तो आप उस प्रवंचना को कर सकते हैं, लेकिन यह केवल एक संकलक विस्तार के साथ पूरा किया जा सकता है।
wizzwizz4

25

यह एक उत्तर नहीं है, लेकिन अगर मैं टिप्पणी में परिणाम डालूं तो पढ़ना मुश्किल है।

मैं एक मैक प्रो ( Westmere 6-Cores Xeon 3.33 GHz) के साथ ये परिणाम प्राप्त करता हूं । मैंने इसे संकलित किया clang -O3 -msse4 -lstdc++ a.cpp -o a(-ओ 2 को समान परिणाम मिला)।

के साथ उलझना uint64_t size=atol(argv[1])<<20;

unsigned    41950110000 0.811198 sec    12.9263 GB/s
uint64_t    41950110000 0.622884 sec    16.8342 GB/s

के साथ उलझना uint64_t size=1<<20;

unsigned    41950110000 0.623406 sec    16.8201 GB/s
uint64_t    41950110000 0.623685 sec    16.8126 GB/s

मैंने भी करने की कोशिश की:

  1. परीक्षण क्रम को उल्टा करें, परिणाम समान है इसलिए यह कैश फैक्टर को नियंत्रित करता है।
  2. है forरिवर्स में बयान: for (uint64_t i=size/8;i>0;i-=4)। यह एक ही परिणाम देता है और यह साबित करता है कि संकलन स्मार्ट है जो आकार में 8 से हर पुनरावृत्ति (अपेक्षा के अनुसार) को विभाजित नहीं करता है।

यहाँ मेरा जंगली अनुमान है:

गति कारक तीन भागों में आता है:

  • कोड कैश: uint64_tसंस्करण का बड़ा कोड आकार होता है, लेकिन इससे मेरे एक्सोन सीपीयू पर प्रभाव नहीं पड़ता है। यह 64-बिट संस्करण को धीमा बनाता है।

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

  • निर्देश केवल 64-बिट कंपाइल (यानी, प्रीफैच) पर उत्सर्जित होते हैं। यह 64-बिट को तेज़ बनाता है।

तीन कारक एक साथ देखे गए परस्पर विरोधी परिणामों के साथ मेल खाते हैं।


4
दिलचस्प है, क्या आप संकलक संस्करण और संकलक झंडे जोड़ सकते हैं? सबसे अच्छी बात यह है कि आपकी मशीन पर, परिणाम चारों ओर मुड़ जाते हैं, अर्थात, यू 64 का उपयोग करना तेज है । अब तक, मैंने कभी नहीं सोचा कि मेरा लूप वेरिएबल किस प्रकार का है, लेकिन ऐसा लगता है कि मुझे अगली बार दो बार सोचना है :)।
gexicide

2
@gexicide: मैं 16.8201 से 16.8126 तक छलांग नहीं लगाऊंगा, जिससे यह "तेज" होगा।
user541686

2
@Mehrdad: कूद मेरा मतलब है के बीच में से एक है 12.9और 16.8, इसलिए unsignedतेजी से यहाँ है। मेरे बेंचमार्क में, विपरीत मामला था, यानी 26 के लिए unsigned, 15 के लिएuint64_t
gexicide

@gexicide क्या आपने बफर [i] को संबोधित करने में अंतर देखा है?
गैर-नकाबपोश इंटरप्ट

@ कलालिन: नहीं, आपका क्या मतलब है?
gexicide 12

10

मैं एक आधिकारिक जवाब नहीं दे सकता, लेकिन एक संभावित कारण का अवलोकन प्रदान कर सकता हूं। यह संदर्भ स्पष्ट रूप से दर्शाता है कि आपके लूप के शरीर में निर्देशों के लिए विलंबता और प्रवाह के बीच 3: 1 अनुपात है। यह कई प्रेषण के प्रभावों को भी दर्शाता है। चूँकि आधुनिक x86 प्रोसेसर में तीन पूर्णांक इकाइयाँ (देना या लेना) हैं, इसलिए आमतौर पर प्रति चक्र तीन निर्देशों को भेजना संभव है।

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

64-बिट सही पारियों के लिए पेंटियम 4 का प्रदर्शन वास्तव में खराब है। 64-बिट लेफ्ट शिफ्ट के साथ-साथ सभी 32-बिट शिफ्ट में स्वीकार्य प्रदर्शन है। ऐसा प्रतीत होता है कि ALU के निचले 32 बिट्स के ऊपरी 32 बिट्स से डेटा पथ अच्छी तरह से डिज़ाइन नहीं किया गया है।

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

यहाँ मेरा अनुमान पूर्णांक इकाइयों के लिए विवाद है: कि popcnt, लूप काउंटर, और पता गणना सभी पूरी तरह से 32-बिट चौड़े काउंटर के साथ पूरी गति से चल सकते हैं, लेकिन 64-बिट काउंटर विवाद और पाइपलाइन स्टालों का कारण बनता है। चूँकि कुल लगभग 12 चक्र हैं, संभावित 4 चक्र कई प्रेषण के साथ, प्रति लूप शरीर के निष्पादन के साथ, एक स्टाल यथोचित रूप से 2 के कारक द्वारा चलने के समय को प्रभावित कर सकता है।

स्थैतिक चर का उपयोग करके प्रेरित परिवर्तन, जिसका मैं अनुमान लगा रहा हूं कि केवल निर्देशों का एक मामूली पुनरावर्तन है, एक और सुराग है कि 32-बिट कोड विवाद के लिए कुछ टिपिंग बिंदु पर है।

मैं जानता हूँ कि यह एक कठोर विश्लेषण नहीं है, लेकिन यह है एक प्रशंसनीय विवरण।


2
दुर्भाग्य से, कभी (कोर 2?) के बाद से 32-बिट और 64-बिट पूर्णांक ऑपरेशन के बीच कोई प्रदर्शन अंतर नहीं हैं सिवाय गुणा / फूट के - जो इस कोड में मौजूद नहीं हैं।
रहस्यवादी

@Gene: ध्यान दें कि सभी संस्करण एक रजिस्टर में आकार को संग्रहीत करते हैं और इसे लूप में स्टैक से कभी नहीं पढ़ते हैं। इस प्रकार, पता गणना मिश्रण में नहीं हो सकती है, कम से कम लूप के अंदर नहीं।
gexicide

@ दृश्य: वास्तव में दिलचस्प व्याख्या! लेकिन यह मुख्य डब्ल्यूटीएफ बिंदुओं की व्याख्या नहीं करता है: पाइपलाइन स्टालों के कारण 64 बिट 32 बिट से धीमी है। लेकिन अगर यह मामला है, तो 64 बिट संस्करण को 32 बिट की तुलना में मज़बूती से धीमा नहीं होना चाहिए ? इसके बजाय, तीन अलग-अलग संकलक 32-बिट संस्करण के लिए भी धीमा कोड का उत्सर्जन करते हैं, जब संकलन-समय-स्थिर बफर आकार का उपयोग करते हैं; बफर के आकार को फिर से स्थिर करने से चीजें पूरी तरह से बदल जाती हैं। मेरे सहयोगियों की मशीन (और केल्विन के उत्तर में) पर एक मामला भी था, जहां 64 बिट संस्करण काफी तेज है! यह बिल्कुल अप्रत्याशित लगता है ..
gexicide

@ मिस्टिक यह मेरी बात है। IU, बस समय, आदि के लिए शून्य विवाद होने पर कोई चरम प्रदर्शन अंतर नहीं है। संदर्भ स्पष्ट रूप से दिखाता है। ध्यान हर चीज को अलग बनाता है। इंटेल कोर साहित्य से एक उदाहरण यहां दिया गया है: "डिजाइन में शामिल एक नई तकनीक मैक्रो-ऑप्स फ्यूजन है, जो दो x86 निर्देशों को एक ही माइक्रो-ऑपरेशन में जोड़ती है। उदाहरण के लिए, एक सशर्त छलांग के बाद एक तुलना की तरह एक सामान्य कोड अनुक्रम। एक एकल माइक्रो-ऑप बन जाएगा। दुर्भाग्य से, यह तकनीक 64-बिट मोड में काम नहीं करती है। ” इसलिए हमारे पास निष्पादन गति में 2: 1 का अनुपात है।
जीन

@gexicide मैं देख रहा हूं कि आप क्या कह रहे हैं, लेकिन आप जितना मेरा मतलब है उससे अधिक का अनुमान लगा रहे हैं। मैं कह रहा हूं कि जो कोड सबसे तेज चल रहा है, वह पाइपलाइन और डिस्पैच कतारों को पूरा कर रहा है। यह हालत नाजुक है। कुल डेटा प्रवाह में 32 बिट्स जोड़ने जैसे मामूली बदलाव और इसे तोड़ने के लिए निर्देश पुन: व्यवस्थित करने के लिए पर्याप्त हैं। संक्षेप में, ओपी का कहना है कि आगे बढ़ना और परीक्षण करना एकमात्र तरीका सही है।
जीन

10

मैंने विजुअल स्टूडियो 2013 एक्सप्रेस के साथ इंडेक्स के बजाय पॉइंटर का उपयोग करके यह कोशिश की , जिसने इस प्रक्रिया को थोड़ा सा बढ़ा दिया। मुझे इस पर संदेह है, क्योंकि ऑफसेट + रजिस्टर + (रजिस्टर << 3) के बजाय एड्रेसिंग ऑफसेट + रजिस्टर है। C ++ कोड।

   uint64_t* bfrend = buffer+(size/8);
   uint64_t* bfrptr;

// ...

   {
      startP = chrono::system_clock::now();
      count = 0;
      for (unsigned k = 0; k < 10000; k++){
         // Tight unrolled loop with uint64_t
         for (bfrptr = buffer; bfrptr < bfrend;){
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
            count += __popcnt64(*bfrptr++);
         }
      }
      endP = chrono::system_clock::now();
      duration = chrono::duration_cast<std::chrono::nanoseconds>(endP-startP).count();
      cout << "uint64_t\t"  << count << '\t' << (duration/1.0E9) << " sec \t"
           << (10000.0*size)/(duration) << " GB/s" << endl;
   }

असेंबली कोड: r10 = bfrptr, r15 = bfrend, rsi = count, rdi = बफर, r15 = b:

$LL5@main:
        mov     r10, rdi
        cmp     rdi, r15
        jae     SHORT $LN4@main
        npad    4
$LL2@main:
        mov     rax, QWORD PTR [r10+24]
        mov     rcx, QWORD PTR [r10+16]
        mov     r8, QWORD PTR [r10+8]
        mov     r9, QWORD PTR [r10]
        popcnt  rdx, rax
        popcnt  rax, rcx
        add     rdx, rax
        popcnt  rax, r8
        add     r10, 32
        add     rdx, rax
        popcnt  rax, r9
        add     rsi, rax
        add     rsi, rdx
        cmp     r10, r15
        jb      SHORT $LL2@main
$LN4@main:
        dec     r13
        jne     SHORT $LL5@main

9

क्या आपने -funroll-loops -fprefetch-loop-arraysजीसीसी पास करने की कोशिश की है ?

मुझे इन अतिरिक्त अनुकूलन के साथ निम्नलिखित परिणाम मिलते हैं:

[1829] /tmp/so_25078285 $ cat /proc/cpuinfo |grep CPU|head -n1
model name      : Intel(R) Core(TM) i3-3225 CPU @ 3.30GHz
[1829] /tmp/so_25078285 $ g++ --version|head -n1
g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3

[1829] /tmp/so_25078285 $ g++ -O3 -march=native -std=c++11 test.cpp -o test_o3
[1829] /tmp/so_25078285 $ g++ -O3 -march=native -funroll-loops -fprefetch-loop-arrays -std=c++11     test.cpp -o test_o3_unroll_loops__and__prefetch_loop_arrays

[1829] /tmp/so_25078285 $ ./test_o3 1
unsigned        41959360000     0.595 sec       17.6231 GB/s
uint64_t        41959360000     0.898626 sec    11.6687 GB/s

[1829] /tmp/so_25078285 $ ./test_o3_unroll_loops__and__prefetch_loop_arrays 1
unsigned        41959360000     0.618222 sec    16.9612 GB/s
uint64_t        41959360000     0.407304 sec    25.7443 GB/s

3
लेकिन फिर भी, आपके परिणाम पूरी तरह से अजीब हैं (पहले अहस्ताक्षरित तेजी से, फिर uint64_t तेजी से) के रूप में अनियंत्रित होकर झूठी निर्भरता की मुख्य समस्या को ठीक नहीं करता है।
जननाशक

7

क्या आपने लूप के बाहर कमी कदम को चलाने की कोशिश की है? अभी आपके पास एक डेटा निर्भरता है जिसकी वास्तव में आवश्यकता नहीं है।

प्रयत्न:

  uint64_t subset_counts[4] = {};
  for( unsigned k = 0; k < 10000; k++){
     // Tight unrolled loop with unsigned
     unsigned i=0;
     while (i < size/8) {
        subset_counts[0] += _mm_popcnt_u64(buffer[i]);
        subset_counts[1] += _mm_popcnt_u64(buffer[i+1]);
        subset_counts[2] += _mm_popcnt_u64(buffer[i+2]);
        subset_counts[3] += _mm_popcnt_u64(buffer[i+3]);
        i += 4;
     }
  }
  count = subset_counts[0] + subset_counts[1] + subset_counts[2] + subset_counts[3];

आपके पास कुछ अजीब अलियासिंग भी चल रहे हैं, मुझे यकीन नहीं है कि सख्त अलियासिंग नियमों के अनुरूप है।


2
यह पहला काम था जो मैंने सवाल पढ़ने के बाद किया है। निर्भरता श्रृंखला को तोड़ें। जैसा कि यह निकला कि प्रदर्शन अंतर नहीं बदलता है (कम से कम मेरे कंप्यूटर पर - जीसीसी 4.7.3 के साथ इंटेल हैसवेल)।
निल्स पिपेनब्रिंक

1
@BenVoigt: यह सख्त अलियासिंग के अनुरूप है। void*और char*दो प्रकार के होते हैं जिन्हें अलियास किया जा सकता है, क्योंकि वे संभावित रूप से "संकेतकर्ता को स्मृति के कुछ भाग में" माना जाता है! डेटा निर्भरता हटाने से संबंधित आपका विचार अनुकूलन के लिए अच्छा है, लेकिन यह प्रश्न का उत्तर नहीं देता है। और, जैसा @NilsPipenbrinck कहते हैं, यह कुछ भी बदलने के लिए नहीं लगता है।
gexicide

@gexicide: सख्त अलियासिंग नियम सममित नहीं है। आप एक का उपयोग char*करने के लिए उपयोग कर सकते हैं T[]। आप सुरक्षित रूप से एक का उपयोग करने के लिए उपयोग नहीं कर सकते हैं , और आपका कोड बाद का काम करता है। T*char[]
बेन वोइगट

@BenVoigt: तब आप कभी mallocभी किसी चीज़ की एक सरणी नहीं बचा सकते थे , जैसा कि मॉलॉक देता है void*और आप इसकी व्याख्या करते हैं T[]। और मैं बहुत यकीन है कि कर रहा हूँ void*और char*सख्त अलियासिंग के विषय में एक ही अर्थ विज्ञान था। हालाँकि, मुझे लगता है कि यह यहाँ काफी
अपमानजनक है

1
व्यक्तिगत रूप से मुझे लगता है कि सही तरीका हैuint64_t* buffer = new uint64_t[size/8]; /* type is clearly uint64_t[] */ char* charbuffer=reinterpret_cast<char*>(buffer); /* aliasing a uint64_t[] with char* is safe */
बेन Voigt

6

टीएल; डीआर: __builtinइसके बजाय आंतरिक का उपयोग करें ; वे मदद करने के लिए हो सकता है।

मैं gcc4.8.4 (और यहां तक ​​कि 4.7.3 gcc.godbolt.org पर) बनाने में सक्षम था, इसके लिए इष्टतम कोड उत्पन्न __builtin_popcountllकरता है जिसका उपयोग करके एक ही विधानसभा निर्देश का उपयोग करता है, लेकिन भाग्यशाली होता है और कोड बनाने के लिए होता है जिसमें अप्रत्याशित रूप से नहीं होता है झूठी निर्भरता बग के कारण लम्बी लूप-निर्भरता।

मैं अपने बेंचमार्किंग कोड का 100% निश्चित नहीं हूं, लेकिन objdumpआउटपुट मेरे विचारों को साझा करता है। मैं किसी भी निर्देश (अजीब व्यवहार, मुझे कहना होगा) के बिना मेरे लिए संकलक अनियंत्रित लूप बनाने के लिए कुछ अन्य ट्रिक्स ( ++iबनाम i++) का उपयोग करता movlहूं।

परिणाम:

Count: 20318230000  Elapsed: 0.411156 seconds   Speed: 25.503118 GB/s

बेंचमार्किंग कोड:

#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

uint64_t builtin_popcnt(const uint64_t* buf, size_t len){
  uint64_t cnt = 0;
  for(size_t i = 0; i < len; ++i){
    cnt += __builtin_popcountll(buf[i]);
  }
  return cnt;
}

int main(int argc, char** argv){
  if(argc != 2){
    printf("Usage: %s <buffer size in MB>\n", argv[0]);
    return -1;
  }
  uint64_t size = atol(argv[1]) << 20;
  uint64_t* buffer = (uint64_t*)malloc((size/8)*sizeof(*buffer));

  // Spoil copy-on-write memory allocation on *nix
  for (size_t i = 0; i < (size / 8); i++) {
    buffer[i] = random();
  }
  uint64_t count = 0;
  clock_t tic = clock();
  for(size_t i = 0; i < 10000; ++i){
    count += builtin_popcnt(buffer, size/8);
  }
  clock_t toc = clock();
  printf("Count: %lu\tElapsed: %f seconds\tSpeed: %f GB/s\n", count, (double)(toc - tic) / CLOCKS_PER_SEC, ((10000.0*size)/(((double)(toc - tic)*1e+9) / CLOCKS_PER_SEC)));
  return 0;
}

संकलन विकल्प:

gcc --std=gnu99 -mpopcnt -O3 -funroll-loops -march=native bench.c -o bench

GCC संस्करण:

gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

लिनक्स कर्नेल संस्करण:

3.19.0-58-generic

CPU जानकारी:

processor   : 0
vendor_id   : GenuineIntel
cpu family  : 6
model       : 70
model name  : Intel(R) Core(TM) i7-4870HQ CPU @ 2.50 GHz
stepping    : 1
microcode   : 0xf
cpu MHz     : 2494.226
cache size  : 6144 KB
physical id : 0
siblings    : 1
core id     : 0
cpu cores   : 1
apicid      : 0
initial apicid  : 0
fpu     : yes
fpu_exception   : yes
cpuid level : 13
wp      : yes
flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx rdtscp lm constant_tsc nopl xtopology nonstop_tsc eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm arat pln pts dtherm fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 invpcid xsaveopt
bugs        :
bogomips    : 4988.45
clflush size    : 64
cache_alignment : 64
address sizes   : 36 bits physical, 48 bits virtual
power management:

3
यह सिर्फ सौभाग्य है कि -funroll-loopsऐसा कोड बनाने के लिए होता है जो लूप-ए निर्भर डिप्रेशन चेन पर popcntझूठ बोलना नहीं है। एक पुराने संकलक संस्करण का उपयोग करना जो झूठी निर्भरता के बारे में नहीं जानता है एक जोखिम है। बिना -funroll-loops, जीसीसी 4.8.5 का लूप थ्रूपुट के बजाय पॉपकंट लेटेंसी पर अड़चन देगा, क्योंकि यह मायने रखता हैrdx4.9.3 द्वारा संकलित समान कोड, xor edx,edxनिर्भरता श्रृंखला को तोड़ने के लिए एक जोड़ता है ।
पीटर कॉर्ड्स

3
पुराने संकलक के साथ, आपका कोड अभी भी वही प्रदर्शन भिन्नता ओपी के अनुभव के अनुरूप होगा: प्रतीत होता है कि तुच्छ परिवर्तन कुछ धीमी गति से कर सकते हैं क्योंकि यह पता नहीं था कि यह एक समस्या का कारण होगा। एक पुराने संकलक पर एक मामले में काम करने के लिए कुछ ऐसा करना सवाल नहीं है।
पीटर कॉर्ड्स

2
रिकॉर्ड के लिए, x86intrin.hके _mm_popcnt_*जीसीसी पर कार्यों को जबरन आसपास रैपर inlined कर रहे हैं__builtin_popcount* ; इनलाइनिंग को एक को दूसरे के बराबर बनाना चाहिए। मुझे बहुत संदेह है कि आप उन दोनों के बीच स्विच करने के कारण कोई अंतर देख सकते हैं।
शैडो रेंजर 15

-2

सबसे पहले, चोटी के प्रदर्शन का अनुमान लगाने का प्रयास करें - https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-altectures-optimization-manual.pdf की जांच करें , विशेष रूप से, परिशिष्ट सी।

आपके मामले में, यह तालिका C-10 है जो दिखाता है कि POPCNT अनुदेश में विलंबता = 3 घड़ियां और थ्रूपुट = 1 घड़ी है। थ्रूपुट घड़ियों में आपकी अधिकतम दर दिखाता है (कोर आवृत्ति द्वारा गुणा करें और पॉपकान्ट 64 के मामले में 8 बाइट्स अपना सर्वश्रेष्ठ संभव बैंडविड्थ नंबर प्राप्त करें)।

अब जांच लें कि संकलक ने क्या किया और लूप में अन्य सभी निर्देशों के थ्रूपुट को समेटा। यह उत्पन्न कोड के लिए सर्वोत्तम संभव अनुमान देगा।

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

हालाँकि, आपके मामले में, सिर्फ सही तरीके से कोड लिखना इन सभी जटिलताओं को खत्म कर देगा। एक ही गणना चर में जमा होने के बजाय, बस अलग-अलग लोगों (जैसे count0, count1, ... count8) को जमा करें और उन्हें अंत में योग करें। या यहां तक ​​कि मायने रखता है [8] की एक सरणी बनाने और अपने तत्वों को जमा - शायद, यह भी वेक्टर किया जाएगा और आप बहुत बेहतर थ्रूपुट प्राप्त करेंगे।

पुनश्च और कभी भी दूसरे के लिए बेंचमार्क न चलाएं, पहले कोर को गर्म करें फिर कम से कम 10 सेकंड या बेहतर 100 सेकंड के लिए लूप को चलाएं। अन्यथा, आप हार्डवेयर में पावर मैनेजमेंट फर्मवेयर और DVFS कार्यान्वयन का परीक्षण करेंगे :)

PPS मैंने इस बात पर अंतहीन बहस सुनी कि बेंचमार्क को वास्तव में कितना समय चलना चाहिए। अधिकांश होशियार लोग यहां तक ​​पूछ रहे हैं कि 10 सेकंड 11 या 12 क्यों नहीं होना चाहिए। मुझे यह मानना ​​चाहिए कि यह सिद्धांत रूप में मज़ेदार है। व्यवहार में, आप सिर्फ एक पंक्ति में सौ बार बेंचमार्क चलाते हैं और विचलन रिकॉर्ड करते हैं। वह आई.एस. अजीब। अधिकांश लोग स्रोत बदलते हैं और इसके बाद बेंच चलाते हैं, बिल्कुल नए प्रदर्शन रिकॉर्ड पर कब्जा करने के लिए। सही काम सही से करें।

अभी भी यकीन नहीं हुआ? बस बेंचमार्क के सी-वर्जन से ऊपर assp1r1n3 ( https://stackoverflow.com/a/37026212/9706746 ) का उपयोग करें ) और लूप में 10000 के बजाय 100 का प्रयास करें।

मेरा 7960X शो, RETRY = 100 के साथ:

गणना: 203182300 समाप्त: 0.008385 सेकंड की गति: 12.505379 जीबी / एस

गणना: 203182300 बीमित: 0.011063 सेकंड की गति: 9.478225 जीबी / एस

गणना: 203182300 बीमित: 0.011188 सेकंड की गति: 9.372327 GB / s

गणना: २०३१ :२३०० व्यपगत: होप ३ ९ ३ सेकंड गति: १०.०2 ९ २५२ जीबी / एस

गणना: 203182300 समाप्त: 0.009076 सेकंड की गति: 11.553283 GB / s

RETRY = 10000 के साथ:

गणना: 20318230000 समाप्त: 0.661791 सेकंड की गति: 15.844519 जीबी / एस

गणना: 20318230000 समाप्त: 0.665422 सेकंड गति: 15.758060 जीबी / एस

गणना: 20318230000 समाप्त: 0.660983 सेकंड की गति: 15.863888 जीबी / एस

गणना: 20318230000 समाप्त: 0.665337 सेकंड की गति: 15.760073 जीबी / एस

गणना: 20318230000 समाप्त: 0.662138 सेकंड की गति: 15.836215 जीबी / एस

PPPS अंत में, "स्वीकृत उत्तर" और अन्य मिस्टी ;-) पर

चलो assp1r1n3 के उत्तर का उपयोग करें - उसके पास 2.5Ghz कोर है। POPCNT में 1 घड़ी throuhgput है, उसका कोड 64-बिट popcnt का उपयोग कर रहा है। इसलिए गणित अपने सेटअप के लिए 2.5Ghz * 1 घड़ी * 8 बाइट्स = 20 GB / s है। वह 25Gb / s देख रहा है, शायद 3Ghz के आसपास टर्बो बूस्ट के कारण।

इस प्रकार ark.intel.com पर जाएं और i7-4870HQ देखें: https://ark.intel.com/products/83504/Intel-Core-i7-4870HQ-Processor-6M-Cache-up-to-3-70 -GHz-? q = i7-4870HQ

वह कोर 3.7Ghz तक चल सकता है और वास्तविक अधिकतम दर उसके हार्डवेयर के लिए 29.6 GB / s है। तो दूसरा 4GB / s कहां है? शायद, यह प्रत्येक तर्क के भीतर लूप लॉजिक और अन्य आसपास के कोड पर खर्च किया जाता है।

अब यह झूठी निर्भरता कहां है ? हार्डवेयर लगभग चरम दर पर चलता है। शायद मेरा गणित खराब है, यह कभी-कभी होता है :)

PPPPPS फिर भी लोगों का सुझाव है कि एचडब्ल्यू इरेटा अपराधी है, इसलिए मैं सुझाव का पालन करता हूं और इनलाइन asm उदाहरण बनाया है, नीचे देखें।

मेरे 7960X पर, पहला संस्करण (सिंगल आउटपुट के साथ cnt0) 11MB / s पर, दूसरा संस्करण (cnt0, cnt1, cnt2 और cnt3 के आउटपुट के साथ) 33MB / s पर चलता है। और कोई कह सकता था - वायली! यह उत्पादन निर्भरता है।

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

uint64_t builtin_popcnt1a(const uint64_t* buf, size_t len) 
{
    uint64_t cnt0, cnt1, cnt2, cnt3;
    cnt0 = cnt1 = cnt2 = cnt3 = 0;
    uint64_t val = buf[0];
    #if 0
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "popcnt %2, %1\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0)
        : "q" (val)
        :
        );
    #else
        __asm__ __volatile__ (
            "1:\n\t"
            "popcnt %5, %1\n\t"
            "popcnt %5, %2\n\t"
            "popcnt %5, %3\n\t"
            "popcnt %5, %4\n\t"
            "subq $4, %0\n\t"
            "jnz 1b\n\t"
        : "+q" (len), "=q" (cnt0), "=q" (cnt1), "=q" (cnt2), "=q" (cnt3)
        : "q" (val)
        :
        );
    #endif
    return cnt0;
}

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

Clang AVX2 के __builtin_popcountlसाथ ऑटो-वेक्टर कर सकता है vpshufb, और ऐसा करने के लिए C स्रोत में कई संचयकों की आवश्यकता नहीं है। मुझे यकीन नहीं है _mm_popcnt_u64; केवल AVX512-VPOPCNT के साथ ऑटो-वेक्टर हो सकता है। ( AVX-512 या AVX-2 / का उपयोग करके बड़े डेटा पर 1 बिट्स (जनसंख्या गणना) देखें
पीटर कॉर्ड्स

लेकिन वैसे भी, इंटेल के अनुकूलन मैनुअल को देखने से मदद नहीं मिलेगी: स्वीकार किए गए उत्तर शो के रूप में, समस्या एक अप्रत्याशित आउटपुट निर्भरता है popcnt। यह इंटेल के इरेटा में उनके हाल ही के कुछ माइक्रोआर्किटेक्चर्स के लिए प्रलेखित है, लेकिन मुझे लगता है कि उस समय नहीं था। यदि अप्रत्याशित झूठी निर्भरताएं हैं, तो आपकी प्रति-श्रृंखला विश्लेषण विफल हो जाएगा, इसलिए यह उत्तर अच्छी जेनेरिक सलाह है लेकिन यहां लागू नहीं है।
पीटर कॉर्ड्स

1
क्या आप मेरे साथ मजाक कर रहे हैं? मुझे उन चीजों पर "विश्वास" करने की ज़रूरत नहीं है जिन्हें मैं हाथ से लिखे हुए ऐश लूप में प्रदर्शन काउंटरों के साथ प्रयोगात्मक रूप से माप सकता हूं। वे सिर्फ तथ्य हैं। मैंने परीक्षण किया है, और स्काईलेक ने lzcnt/ के लिए झूठी निर्भरता तय की है tzcnt, लेकिन इसके लिए नहीं popcnt। में इंटेल की इरेटा SKL029 देखें intel.com/content/dam/www/public/us/en/documents/... । इसके अलावा, gcc.gnu.org/bugzilla/show_bug.cgi?id=62011 "फिक्स्ड फिक्स्ड" है, न कि "अमान्य"। आपके दावे का कोई आधार नहीं है कि HW में कोई आउटपुट निर्भरता नहीं है।
पीटर कॉर्ड्स

1
यदि आप एक साधारण लूप बनाते हैं जैसे popcnt eax, edx/ dec ecx / jnz, तो आप उम्मीद करेंगे कि यह प्रति घड़ी 1 बजे चले, popcnt थ्रूपुट और ली गई ब्रांच थ्रूपुट पर अड़चन। लेकिन यह वास्तव में केवल 1 प्रति 3 घड़ियों पर चलता है जो popcntबार-बार ईएएक्स को ओवरराइट करने के लिए विलंबता पर अड़ जाता है, भले ही आप इसे केवल लिखने की उम्मीद करेंगे। आपके पास एक Skylake है, इसलिए आप इसे स्वयं आज़मा सकते हैं।
पीटर कॉर्ड्स

-3

ठीक है, मैं उन उप-प्रश्नों में से एक का एक छोटा सा जवाब देना चाहता हूं जो ओपी ने पूछा था कि मौजूदा प्रश्नों में पता नहीं लगता है। कैविएट, मैंने कोई परीक्षण या कोड जेनरेशन, या डिसएस्पेशन नहीं किया है, बस दूसरों के लिए एक विचार साझा करना चाहता था।

क्यों staticबदलता है प्रदर्शन?

प्रश्न में पंक्ति: uint64_t size = atol(argv[1])<<20;

संक्षिप्त जवाब

मैं पहुंच के लिए उत्पन्न विधानसभा को sizeदेखूंगा और यह देखूंगा कि गैर-स्थैतिक संस्करण के लिए सूचक अप्रत्यक्ष के अतिरिक्त चरण शामिल हैं या नहीं।

लंबा जवाब

चूँकि चर की एक ही प्रति है, चाहे वह घोषित की गई हो staticया नहीं, और आकार में परिवर्तन नहीं होता है, मैं यह बताता हूँ कि अंतर चर को वापस करने के लिए उपयोग की जाने वाली स्मृति का स्थान है जहाँ इसे आगे कोड में उपयोग किया जाता है नीचे।

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

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


2
यह मेरे लिए बहुत अधिक संभावना है कि staticफ़ंक्शन के लिए रजिस्टर आवंटन को बदलने का उपयोग इस तरह से किया popcntगया है कि इंटेल सीपीयू पर झूठी आउटपुट निर्भरता को प्रभावित करता है , ओपी परीक्षण कर रहा था, एक संकलक के साथ जो उनसे बचने के लिए नहीं जानता था। (क्योंकि इंटेल सीपीयू में इस प्रदर्शन का गड्ढा अभी तक खोजा नहीं गया था।) एक संकलक staticएक स्वचालित चर चर की तरह, एक स्थानीय चर को एक रजिस्टर में रख सकता है, लेकिन अगर वे mainकेवल एक बार चलने वाले अनुमानों का अनुकूलन नहीं करते हैं , तो यह प्रभावित करेगा कोड-जीन (क्योंकि मूल्य केवल पहली कॉल द्वारा निर्धारित होता है।)
पीटर कॉर्डेस

1
वैसे भी, मोड [RIP + rel32]और [rsp + 42]मोड के बीच प्रदर्शन अंतर ज्यादातर मामलों के लिए बहुत नगण्य है। cmp dword [RIP+rel32], immediateएक लोड + सीएमपी यूओपी में माइक्रो-फ्यूज नहीं कर सकते हैं, लेकिन मुझे नहीं लगता कि यह एक कारक होने जा रहा है। जैसा कि मैंने कहा, लूप्स के अंदर यह शायद वैसे भी एक रजिस्टर में रहता है, लेकिन सी ++ को ट्विक करने का मतलब अलग-अलग संकलक विकल्प हो सकते हैं।
पीटर कॉर्डेस
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.