ठीक 8192 तत्वों पर लूपिंग करते समय मेरा कार्यक्रम धीमा क्यों है?


755

यहाँ प्रश्न में कार्यक्रम से अर्क है। मैट्रिक्स img[][]का आकार SIZE × SIZE है, और इसे यहां आरंभ किया गया है:

img[j][i] = 2 * j + i

फिर, आप एक मैट्रिक्स बनाते हैं res[][], और यहां प्रत्येक फ़ील्ड को img मैट्रिक्स में इसके चारों ओर 9 फ़ील्ड का औसत बनाया जाता है। सीमा को सादगी के लिए 0 पर छोड़ दिया जाता है।

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

यह सब वहाँ कार्यक्रम के लिए है। पूर्णता की खातिर, यहाँ पहले जो आता है। इसके बाद कोई कोड नहीं आता है। जैसा कि आप देख सकते हैं, यह सिर्फ आरंभीकरण है।

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

मूल रूप से, यह कार्यक्रम धीमा है जब SIZE 2048 से अधिक है, उदाहरण के लिए निष्पादन समय:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

संकलक जीसीसी है। मैं जो जानता हूं, यह स्मृति प्रबंधन के कारण है, लेकिन मैं वास्तव में उस विषय के बारे में बहुत अधिक नहीं जानता हूं, यही कारण है कि मैं यहां पूछ रहा हूं।

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

मैं पहले से ही मॉलॉक / मुफ्त का पता है, लेकिन समस्या का उपयोग की गई मेमोरी की मात्रा नहीं है, यह केवल निष्पादन का समय है, इसलिए मुझे नहीं पता कि यह कैसे मदद करेगा।


67
@ बोकन तब होता है जब आकार कैश के महत्वपूर्ण स्ट्राइड का एक से अधिक होता है।
लुचियन ग्रिगोर 14

5
@ मूल, यह कोई फर्क नहीं पड़ता, यह एक ही सटीक समस्या को उजागर करता है; कोड अलग हो सकता है, लेकिन मूल रूप से दोनों प्रश्न एक ही समय के बारे में पूछते हैं (और उनके शीर्षक निश्चित रूप से समान हैं)।
14

33
यदि आप उच्च प्रदर्शन चाहते हैं, तो आपको 2 आयाम सरणी का उपयोग करके छवि को संसाधित नहीं करना चाहिए। सभी पिक्सेल कच्चे होने पर विचार करें और उन्हें एक आयाम सरणी की तरह संसाधित करें। इस ब्लर को दो पास में करें। पहले 3 पिक्सेल की स्लाइडिंग राशि का उपयोग करके आसपास के पिक्सेल का मूल्य जोड़ें: स्लाइडसम + = src [i + 1] -src [i-1]; गंतव्य [i] = slideSum ;. फिर एक ही तरह से लंबवत करें और एक ही समय में विभाजित करें: गंतव्य [i] = (src [i-width] + src [i] + src [i + width]) / 9। www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan

8
यहाँ वास्तव में दो चीजें चल रही हैं। यह केवल सुपर-संरेखण नहीं है।
रहस्यमय

7
(आपके उत्तर पर बस एक छोटी सी निप्पिक। पहले कोड सेगमेंट के लिए, यह अच्छा होगा यदि आपके सभी छोरों में ब्रेसिज़ होते हैं।)
ट्रेवर बोयड स्मिथ

जवाबों:


954

अंतर निम्नलिखित संबंधित प्रश्नों से एक ही सुपर-संरेखण मुद्दे के कारण होता है:

लेकिन यह केवल इसलिए है क्योंकि कोड के साथ एक और समस्या है।

मूल पाश से शुरू:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

पहले ध्यान दें कि दो आंतरिक छोर तुच्छ हैं। उन्हें निम्नानुसार अनियंत्रित किया जा सकता है:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

ताकि दो बाहरी-छोरों को छोड़ दें जिनकी हम रुचि रखते हैं।

अब हम देख सकते हैं कि इस प्रश्न में समस्या समान है: 2 डी सरणी पर पुनरावृति होने पर लूप का क्रम प्रदर्शन को क्यों प्रभावित करता है?

आप पंक्ति-वार के बजाय मैट्रिक्स कॉलम-वार को पुनरावृत्त कर रहे हैं।


इस समस्या को हल करने के लिए, आपको दो छोरों को बदलना चाहिए।

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

यह सभी गैर-अनुक्रमिक पहुंच को पूरी तरह से समाप्त कर देता है ताकि आप अब बड़ी शक्तियों में से दो पर यादृच्छिक धीमी गति से न उतरें।


कोर i7 920 @ 3.5 GHz

मूल कोड:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

इंटरचेंज्ड आउटर-लूप्स:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
मैं यह भी ध्यान देता हूं कि आंतरिक छोरों को अनियंत्रित करने से प्रदर्शन पर कोई प्रभाव नहीं पड़ता है। संकलक शायद यह स्वचालित रूप से करता है। मैंने उन्हें छुटकारा पाने के एकमात्र उद्देश्य के लिए उन्हें अनियंत्रित किया ताकि बाहरी छोरों के साथ समस्या को हल करना आसान हो सके।
16

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

33
@ClickUpvote यह वास्तव में एक हार्डवेयर (कैशिंग) समस्या है। इसका भाषा से कोई लेना-देना नहीं है। यदि आपने इसे किसी अन्य भाषा में आज़माया है जो देशी कोड को संकलित करता है या JIT करता है, तो आपको संभवतः वही प्रभाव दिखाई देंगे।
मिस्ट्रीज

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

154
यह एसओ पर एक अच्छे उत्तर का एक आदर्श उदाहरण है: इसी तरह के सवालों का संदर्भ, चरण-दर-चरण बताता है कि आपने इसे कैसे संपर्क किया, समस्या की व्याख्या करता है, यह बताता है कि समस्या को कैसे ठीक करें, महान स्वरूपण है, और कोड चलाने का एक उदाहरण भी है। आपकी मशीन पर। आपके सहयोग के लिए धन्यवाद।
मत्‍स्‍यार

57

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


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

pointer + (x + y*width)*(sizeOfOnePixel)

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

मैंने कुछ परीक्षण किए हैं और मुझे लगता है कि यह साझा करने लायक है। प्रत्येक परिणाम औसत पांच परीक्षण है।

User1615209 द्वारा मूल कोड:

8193: 4392 ms
8192: 9570 ms

रहस्यमय संस्करण:

8193: 2393 ms
8192: 2190 ms

1 डी सरणी का उपयोग करते हुए दो पास: पहली बार क्षैतिज योगों के लिए, दूसरा ऊर्ध्वाधर योग और औसत के लिए। तीन बिंदुओं के साथ दो पासिंग और इस तरह केवल वेतन वृद्धि:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

1D सरणी का उपयोग करके दो पास और इस तरह संबोधित करना:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

कैश में रुकने से एक पंक्ति पहले क्षैतिज संस्मरण पास करता है ताकि वे कैश में रहें:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

निष्कर्ष:

  • कई पॉइंटर्स और सिर्फ इन्क्रीमेंट का उपयोग करने का कोई लाभ नहीं (मुझे लगता है कि यह तेज़ होता)
  • क्षैतिज योगों को कैशिंग करना कई बार उनकी गणना करने से बेहतर है।
  • दो पास तीन गुना तेज नहीं, केवल दो बार।
  • एकल पास और कैशिंग एक मध्यस्थ परिणाम का उपयोग करके 3.6 गुना तेजी से प्राप्त करना संभव है

मुझे यकीन है कि यह बहुत बेहतर करना संभव है।

नोट कृपया ध्यान दें कि मैं कैश समस्या सामान्य प्रदर्शन के मुद्दों के बजाय लक्षित करने के लिए इस जवाब लिखा मिस्टिकल उत्तम जवाब में विस्तार से बताया। शुरुआत में यह सिर्फ छद्म कोड था। मुझे टिप्पणियों में परीक्षण करने के लिए कहा गया था ... यहां परीक्षणों के साथ एक पूरी तरह से refactored संस्करण है।


9
"मुझे लगता है कि यह कम से कम 3 गुना तेज है" - कुछ मैट्रिक्स या उद्धरण के साथ उस दावे का बैकअप लेने के लिए?
एडम रोसेनफील्ड

8
@AdamRosenfield "मुझे लगता है" = दमन! = "यह" = दावा है। मेरे पास इसके लिए कोई मीट्रिक नहीं है और मैं एक परीक्षण देखना चाहूंगा। लेकिन मेरा 7 पिक्सेल, 2 सब, 2 ऐड और एक डिव प्रति पिक्सेल की आवश्यकता है। सीपीयू में रजिस्टर की तुलना में कम स्थानीय संस्करण का उपयोग करने वाला प्रत्येक लूप। कंपाइलर ऑप्टिमाइज़ेशन के आधार पर दूसरे को 7 वेतन वृद्धि, 6 वेतन वृद्धि, 1 div और 10 से 20 mul के लिए संबोधित करने की आवश्यकता होती है। लूप में प्रत्येक निर्देश को पिछले निर्देश के परिणाम की आवश्यकता होती है, यह पेंटियम के सुपर-स्केलर आर्किटेक्चर के लाभों को छोड़ देता है। इसलिए इसे और तेज करना होगा।
बोकेन

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

1
अच्छा किया, वे कुछ प्रभावशाली संख्याएँ हैं। जैसा कि आपने पाया, यह स्मृति प्रदर्शन के बारे में है - वेतन वृद्धि के साथ कई बिंदुओं का उपयोग करने से कोई लाभ नहीं मिला।
एडम रोसेनफील्ड

2
@AdamRosenfield मैं आज सुबह काफी चिंतित था क्योंकि मैं परीक्षणों को पुन: पेश नहीं कर सका। ऐसा प्रतीत होता है कि प्रदर्शन में वृद्धि केवल विजुअल C ++ कंपाइलर के साथ है। जीसीसी का उपयोग करना, केवल एक छोटा अंतर है।
बोकेन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.