513x513 के मैट्रिक्स को ट्रांसप्लांट करने की तुलना में 512x512 के मैट्रिक्स को अधिक धीमा क्यों किया जाता है?


218

विभिन्न आकारों के वर्ग मैट्रिसेस पर कुछ प्रयोग करने के बाद, एक पैटर्न सामने आया। सदा ही, आकार के एक मैट्रिक्स transposing 2^nआकार में से एक सुर से धीमी है2^n+1 । के छोटे मूल्यों के लिए n, अंतर प्रमुख नहीं है।

हालांकि बड़े अंतर 512 के मान से कम होते हैं। (कम से कम मेरे लिए)

अस्वीकरण: मुझे पता है कि फ़ंक्शन वास्तव में तत्वों के दोहरे स्वैप के कारण मैट्रिक्स को स्थानांतरित नहीं करता है, लेकिन इससे कोई फर्क नहीं पड़ता है।

कोड का अनुसरण करता है:

#define SAMPLES 1000
#define MATSIZE 512

#include <time.h>
#include <iostream>
int mat[MATSIZE][MATSIZE];

void transpose()
{
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
   {
       int aux = mat[i][j];
       mat[i][j] = mat[j][i];
       mat[j][i] = aux;
   }
}

int main()
{
   //initialize matrix
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
       mat[i][j] = i+j;

   int t = clock();
   for ( int i = 0 ; i < SAMPLES ; i++ )
       transpose();
   int elapsed = clock() - t;

   std::cout << "Average for a matrix of " << MATSIZE << ": " << elapsed / SAMPLES;
}

बदलने MATSIZEसे हम आकार में परिवर्तन कर सकते हैं (डुह!)। मैंने ideone पर दो संस्करण पोस्ट किए:

मेरे वातावरण में (MSVS 2010, पूर्ण अनुकूलन), अंतर समान है:

  • आकार 512 - औसत 2.19 एमएस
  • आकार 513 - औसत 0.57 एमएस

ये क्यों हो रहा है?


9
आपका कोड मुझे कैफ़े से दिखता है।
कोडइन्चौस जूल

7
यह इस मुद्दे के रूप में बहुत अधिक एक ही मुद्दा है: stackoverflow.com/questions/7905760/…
12:30 पर मिस्टिक जूल

@CodesInChaos को बढ़ाना, देखभाल करना? (या कोई और।)
कोरज़ा

@ लेन स्वीकार किए गए उत्तर को पढ़ने के बारे में कैसे?
कोडइन्चौस

4
@nzomkxia यह अनुकूलन के बिना कुछ भी मापने के लिए थोड़े व्यर्थ है। अक्षम किए गए अनुकूलन के साथ, उत्पन्न कोड को बाहरी कचरे के साथ लिट किया जाएगा जो अन्य बाधाओं को छिपाएगा। (जैसे कि मेमोरी)
मिस्टीरियल ऑक्ट

जवाबों:


197

स्पष्टीकरण C ++ में ऑप्टिमाइज़िंग सॉफ़्टवेयर में Agner Fog से आता है और यह कम करता है कि कैसे डेटा एक्सेस किया जाता है और कैश में संग्रहीत किया जाता है।

शब्दों और विस्तृत जानकारी के लिए, कैशिंग पर विकी प्रविष्टि देखें , मैं इसे यहाँ संकीर्ण कर रहा हूँ।

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

किसी विशेष मेमोरी पते के लिए, हम गणना कर सकते हैं कि कौन सा सेट इसे सूत्र के साथ दर्पण होना चाहिए:

set = ( address / lineSize ) % numberOfsets

इस तरह का सूत्र आदर्श रूप से सेटों में एक समान वितरण देता है, क्योंकि प्रत्येक मेमोरी एड्रेस को पढ़ने की संभावना है (मैंने आदर्श रूप से कहा था )।

यह स्पष्ट है कि ओवरलैप हो सकता है। कैश मिस के मामले में, मेमोरी को कैश में पढ़ा जाता है और पुराने मूल्य को बदल दिया जाता है। याद रखें कि प्रत्येक सेट में कई पंक्तियाँ होती हैं, जिनमें से हाल ही में उपयोग की गई कम से कम एक नई रीड मेमोरी के साथ ओवरराइट की जाती है।

मैं Agner से कुछ उदाहरण का अनुसरण करने की कोशिश करूँगा:

मान लें कि प्रत्येक सेट में 4 लाइनें हैं, प्रत्येक में 64 बाइट्स हैं। हम पहले पते को पढ़ने का प्रयास करते हैं 0x2710, जो सेट में जाता है 28। और फिर हम भी पते को पढ़ने के लिए प्रयास 0x2F00, 0x3700, 0x3F00और 0x4700। ये सभी एक ही सेट के हैं। पढ़ने से पहले 0x4700, सेट में सभी लाइनों पर कब्जा कर लिया गया होगा। उस मेमोरी को पढ़ना सेट में एक मौजूदा रेखा को दर्शाता है, वह रेखा जो शुरू में पकड़ रही थी 0x2710। समस्या इस तथ्य में निहित है कि हम उन पतों को पढ़ते हैं जो (इस उदाहरण के लिए) 0x800अलग हैं। यह महत्वपूर्ण स्ट्राइड (फिर, इस उदाहरण के लिए) है।

महत्वपूर्ण स्ट्राइड की गणना भी की जा सकती है:

criticalStride = numberOfSets * lineSize

चर criticalStrideया एक ही कैश लाइनों के लिए कई अलग-अलग दावेदार।

यह सिद्धांत हिस्सा है। अगला, स्पष्टीकरण (भी Agner, मैं गलतियाँ करने से बचने के लिए निकटता से इसका अनुसरण कर रहा हूँ):

8x कैश के साथ 4x64 का एक मैट्रिक्स मान लें (याद रखें, प्रभाव कैश के अनुसार अलग-अलग होता है) प्रति सेट 64 बाइट्स की 4 लाइन *। प्रत्येक पंक्ति मैट्रिक्स (64-बिट int) में 8 तत्वों को पकड़ सकती है ।

क्रिटिकल स्ट्राइड 2048 बाइट्स होगा, जो मैट्रिक्स की 4 पंक्तियों (जो मेमोरी में निरंतर है) के अनुरूप है।

मान लें कि हम पंक्ति 28 को संसाधित कर रहे हैं। हम इस पंक्ति के तत्वों को लेने का प्रयास कर रहे हैं और उन्हें स्तंभ 28 से तत्वों के साथ स्वैप कर रहे हैं। पंक्ति के पहले 8 तत्व एक कैश लाइन बनाते हैं, लेकिन वे 8 अलग हो जाएंगे कॉलम 28 में कैश लाइनें। याद रखें, महत्वपूर्ण स्ट्राइड 4 पंक्तियाँ हैं (एक कॉलम में 4 लगातार तत्व)।

जब तत्व 16 कॉलम में पहुंच जाता है (प्रति सेट 4 कैश लाइनें और 4 पंक्तियों के अलावा = परेशानी) तो पूर्व-0 तत्व को कैश से निकाल दिया जाएगा। जब हम स्तंभ के अंत तक पहुँचते हैं, तो पिछली सभी कैश लाइनें खो जाती थीं और अगले तत्व तक पहुँचने पर पुनः लोड करने की आवश्यकता होती थी (पूरी लाइन अधिलेखित हो जाती है)।

ऐसा आकार होना जो महत्वपूर्ण स्ट्राइड के एक से अधिक नहीं है , आपदा के लिए इस आदर्श परिदृश्य को गड़बड़ कर देता है, क्योंकि हम अब ऐसे तत्वों से नहीं निपट रहे हैं जो वर्टिकल पर अलग से महत्वपूर्ण स्ट्राइड हैं, इसलिए कैश रीलोड की संख्या गंभीर रूप से कम हो जाती है।

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


ओह और अगली बार। बस मुझे सीधे लाउंज के माध्यम से पिंग । मुझे SO पर नाम का हर उदाहरण नहीं मिला। :) मैंने इसे केवल आवधिक ईमेल सूचनाओं के माध्यम से देखा।
रहस्यमयी जूल

@Mysticial @Luchian Grigore मेरा एक मित्र मुझसे कहता है कि उसका Intel core i3पीसी gcc 4.6 पर Ubuntu 11.04 i386लगभग उसी प्रदर्शन को प्रदर्शित करता है । और मेरे mingw gcc4.4 के साथ मेरे कंप्यूटर के लिए भी ऐसा ही है , जो चल रहा है । जब यह बड़ा अंतर दिखाएगा। मैं इस खंड को gcc 4.6 के साथ थोड़े पुराने पीसी के साथ संकलित करता हूं , जो चल रहा है । Intel Core 2 Duowindows 7(32)intel centrinoubuntu 12.04 i386
होंग्क्सू चेन

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

which goes in set 24क्या आपको इसके बजाय " सेट इन 28 " से मतलब था ? और क्या आप 32 सेट मानते हैं?
रुस्लान

आप सही हैं, यह 28 है। :) मैंने लिंक किए गए पेपर को भी डबल-चेक किया, मूल विवरण के लिए आप 9.2 कैश संगठन में नेविगेट कर सकते हैं
लुचियन ग्रिगोर

78

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

आपका एल्गोरिथ्म मूल रूप से करता है:

for (int i = 0; i < N; i++) 
   for (int j = 0; j < N; j++) 
        A[j][i] = A[i][j];

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

क्या हम इससे बेहतर कर सकते हैं? हाँ हम कर सकते हैं: इस समस्या के लिए एक सामान्य दृष्टिकोण कैश अनजान एल्गोरिदम है कि जैसा कि नाम कहता है कि विशिष्ट कैश आकार पर निर्भर होने से बचा जाता है] [1]

समाधान इस तरह दिखेगा:

void recursiveTranspose(int i0, int i1, int j0, int j1) {
    int di = i1 - i0, dj = j1 - j0;
    const int LEAFSIZE = 32; // well ok caching still affects this one here
    if (di >= dj && di > LEAFSIZE) {
        int im = (i0 + i1) / 2;
        recursiveTranspose(i0, im, j0, j1);
        recursiveTranspose(im, i1, j0, j1);
    } else if (dj > LEAFSIZE) {
        int jm = (j0 + j1) / 2;
        recursiveTranspose(i0, i1, j0, jm);
        recursiveTranspose(i0, i1, jm, j1);
    } else {
    for (int i = i0; i < i1; i++ )
        for (int j = j0; j < j1; j++ )
            mat[j][i] = mat[i][j];
    }
}

थोड़ा और अधिक जटिल है, लेकिन एक छोटा परीक्षण मेरे प्राचीन e8400 पर काफी दिलचस्प दिखाता है VS2010 x64 रिलीज के लिए, परीक्षण के लिए MATSIZE 8192

int main() {
    LARGE_INTEGER start, end, freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&start);
    recursiveTranspose(0, MATSIZE, 0, MATSIZE);
    QueryPerformanceCounter(&end);
    printf("recursive: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));

    QueryPerformanceCounter(&start);
    transpose();
    QueryPerformanceCounter(&end);
    printf("iterative: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
    return 0;
}

results: 
recursive: 480.58ms
iterative: 3678.46ms

संपादित करें: आकार के प्रभाव के बारे में: यह बहुत कम स्पष्ट है, हालांकि अभी भी कुछ हद तक ध्यान देने योग्य है, ऐसा इसलिए है क्योंकि हम पुनरावृत्ति समाधान का उपयोग 1 के बजाय (1 पुनरावर्ती एल्गोरिदम के लिए सामान्य अनुकूलन) के बजाय कर रहे हैं। अगर हम LEAFSIZE = 1 सेट करते हैं, तो कैश का मेरे लिए कोई प्रभाव नहीं है [ 8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms- कि त्रुटि के मार्जिन के अंदर है, उतार चढ़ाव 100ms क्षेत्र में हैं; यह "बेंचमार्क" कुछ ऐसा नहीं है जिसे मैं पूरी तरह से सही मान चाहता हूं, तो मैं बहुत सहज हो जाऊंगा])

[१] इस सामान के लिए स्रोत: यदि आप लीसेरसन के साथ काम कर रहे हैं और इस पर सह से कोई व्याख्यान नहीं पा सकते हैं .. तो मैं उनके कागजों को एक अच्छा प्रारंभिक बिंदु मानता हूं। उन एल्गोरिदम को अभी भी बहुत कम वर्णित किया गया है - सीएलआर के पास उनके बारे में एक ही फुटनोट है। फिर भी यह लोगों को आश्चर्यचकित करने का एक शानदार तरीका है।


संपादित करें (ध्यान दें: मैं वह नहीं हूं जिसने यह उत्तर पोस्ट किया है; मैं बस इसे जोड़ना चाहता था):
यहां उपरोक्त कोड का पूर्ण C ++ संस्करण है:

template<class InIt, class OutIt>
void transpose(InIt const input, OutIt const output,
    size_t const rows, size_t const columns,
    size_t const r1 = 0, size_t const c1 = 0,
    size_t r2 = ~(size_t) 0, size_t c2 = ~(size_t) 0,
    size_t const leaf = 0x20)
{
    if (!~c2) { c2 = columns - c1; }
    if (!~r2) { r2 = rows - r1; }
    size_t const di = r2 - r1, dj = c2 - c1;
    if (di >= dj && di > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, (r1 + r2) / 2, c2);
        transpose(input, output, rows, columns, (r1 + r2) / 2, c1, r2, c2);
    }
    else if (dj > leaf)
    {
        transpose(input, output, rows, columns, r1, c1, r2, (c1 + c2) / 2);
        transpose(input, output, rows, columns, r1, (c1 + c2) / 2, r2, c2);
    }
    else
    {
        for (ptrdiff_t i1 = (ptrdiff_t) r1, i2 = (ptrdiff_t) (i1 * columns);
            i1 < (ptrdiff_t) r2; ++i1, i2 += (ptrdiff_t) columns)
        {
            for (ptrdiff_t j1 = (ptrdiff_t) c1, j2 = (ptrdiff_t) (j1 * rows);
                j1 < (ptrdiff_t) c2; ++j1, j2 += (ptrdiff_t) rows)
            {
                output[j2 + i1] = input[i2 + j1];
            }
        }
    }
}

2
यह प्रासंगिक होगा यदि आप विभिन्न आकारों के मैट्रिक्स के बीच के समय की तुलना करते हैं, पुनरावर्ती और पुनरावृत्ति नहीं। निर्दिष्ट आकारों के एक मैट्रिक्स पर पुनरावर्ती समाधान का प्रयास करें।
लुचियन ग्रिगोर

@ लुचियन चूंकि आपने पहले ही समझाया था कि वह ऐसा व्यवहार क्यों देख रहा है, जिसके बारे में मुझे लगा कि सामान्य रूप से इस समस्या का एक समाधान पेश करना काफी दिलचस्प है।
वू

क्योंकि, मैं सवाल कर रहा हूं कि एक बड़ी मैट्रिक्स को प्रोसेस करने में कम समय क्यों लगता है,
स्पीडर

@ लुचियन १६३3३ और १६३L४ के बीच अंतर हैं .. २ms बनाम २ for मेरे लिए यहाँ, या लगभग ३.५% - वास्तव में महत्वपूर्ण हैं। और अगर यह होता तो मुझे आश्चर्य होता।
वू

3
यह स्पष्ट करना दिलचस्प हो सकता है कि क्या recursiveTransposeकरता है, अर्थात यह छोटी टाइलों ( LEAFSIZE x LEAFSIZEआयामों) पर काम करके कैश को उतना नहीं भरता है ।
मैथ्यू एम।

60

लुचियन ग्रिगोर के जवाब में स्पष्टीकरण के लिए एक चित्रण के रूप में , यहां मैट्रिक्स कैश उपस्थिति 64x64 और 65x65 मैट्रिसेस के दो मामलों की तरह दिखती है (संख्याओं के विवरण के लिए ऊपर लिंक देखें)।

नीचे दिए गए एनिमेशन में रंग निम्नलिखित हैं:

  • सफेद - कैश में नहीं,
  • हल्का हरा - कैश में,
  • चमकीला हरा - कैश मारा,
  • संतरा - बस रैम से पढ़ें,
  • लाल - कैश मिस।

64x64 मामला:

64x64 मैट्रिक्स के लिए कैश उपस्थिति एनीमेशन

ध्यान दें कि नई पंक्ति में लगभग हर एक्सेस कैशे मिस हो जाता है। और अब यह सामान्य स्थिति में कैसे दिखता है, एक 65x65 मैट्रिक्स:

65x65 मैट्रिक्स के लिए कैश उपस्थिति एनीमेशन

यहां आप देख सकते हैं कि शुरुआती वार्मिंग-अप के बाद अधिकांश एक्सेस कैश हिट हैं। यह सीपीयू कैश सामान्य रूप से काम करने का इरादा है।


उपरोक्त एनिमेशन के लिए फ़्रेम उत्पन्न करने वाला कोड यहाँ देखा जा सकता है


ऊर्ध्वाधर स्कैनिंग कैश हिट पहले मामले में क्यों नहीं बचाए जाते हैं, लेकिन वे दूसरे मामले में हैं? ऐसा लगता है कि दिए गए ब्लॉक को दोनों उदाहरणों में अधिकांश ब्लॉकों के लिए एक बार एक्सेस किया गया है।
जोशिया योडर

मैं @ LuchianGrigore के जवाब से देख सकता हूं कि ऐसा इसलिए है क्योंकि कॉलम की सभी लाइनें एक ही सेट से संबंधित हैं।
योशिय्याह योडर

हाँ, महान चित्रण। मैं देख रहा हूं कि वे समान गति से हैं। लेकिन वास्तव में, वे नहीं हैं, वे नहीं हैं?
kalalaka

@kelalaka हां, एनीमेशन एफपीएस समान है। मैंने मंदी का अनुकरण नहीं किया, केवल रंग यहां महत्वपूर्ण हैं।
रुस्लान

यह दिलचस्प होगा कि दो स्थिर चित्र अलग-अलग कैश सेट को दर्शाते हैं।
जोशियाह योडर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.