2 डी सरणी से अधिक चलने पर लूप का क्रम प्रदर्शन को क्यों प्रभावित करता है?


359

नीचे दो प्रोग्राम हैं जो लगभग समान हैं सिवाय इसके कि मैंने iऔर jचर को चारों ओर घुमाया। वे दोनों अलग-अलग समय में चलते हैं। क्या कोई समझा सकता है कि ऐसा क्यों होता है?

संस्करण 1

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (i = 0; i < 4000; i++) {
    for (j = 0; j < 4000; j++) {
      x[j][i] = i + j; }
  }
}

संस्करण 2

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (j = 0; j < 4000; j++) {
     for (i = 0; i < 4000; i++) {
       x[j][i] = i + j; }
   }
}


7
क्या आप कुछ बेंचमार्क परिणाम जोड़ सकते हैं?
naught101


14
@ n-0101 बेंचमार्क 3 से 10 बार के बीच कहीं भी प्रदर्शन अंतर दिखाएगा। यह मूल C / C ++ है, मैं पूरी तरह से स्तब्ध हूं कि इसे इतने वोट कैसे मिले ...
TC1

12
@ TC1: मुझे नहीं लगता कि यह मूल है; शायद मध्यवर्ती। लेकिन यह कोई आश्चर्य नहीं होना चाहिए कि "मूल" सामान अधिक लोगों के लिए उपयोगी हो जाता है, इसलिए कई अपवोट। इसके अलावा, यह एक सवाल है जो Google के लिए कठिन है, भले ही वह "मूल" हो।
लार्स

जवाबों:


594

जैसा कि अन्य लोगों ने कहा है, मुद्दा सरणी में मेमोरी स्थान के लिए स्टोर है: x[i][j] :। यहाँ अंतर्दृष्टि का एक सा क्यों है:

आपके पास 2-आयामी सरणी है, लेकिन कंप्यूटर में मेमोरी स्वाभाविक रूप से 1-आयामी है। तो जब आप इस तरह से अपने सरणी की कल्पना करते हैं:

0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3

आपका कंप्यूटर इसे एक पंक्ति के रूप में मेमोरी में संग्रहीत करता है:

0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3

दूसरे उदाहरण में, आप पहले नंबर पर लूपिंग द्वारा सरणी का उपयोग करते हैं, अर्थात:

x[0][0] 
        x[0][1]
                x[0][2]
                        x[0][3]
                                x[1][0] etc...

मतलब कि आप उन सभी को क्रम में मार रहे हैं। अब पहले संस्करण को देखें। आप कर रहे हैं:

x[0][0]
                                x[1][0]
                                                                x[2][0]
        x[0][1]
                                        x[1][1] etc...

जिस तरह से सी ने 2-डी सरणी को मेमोरी में रखा है, आप इसे सभी जगह कूदने के लिए कह रहे हैं। लेकिन अब किकर के लिए: यह मामला क्यों है? सभी मेमोरी एक्सेस समान हैं, है ना?

नहीं: कैश की वजह से। आपकी मेमोरी से डेटा सीपीयू में थोड़ा-सा हिस्सा (जिसे 'कैश लाइनें' कहा जाता है) में लाया जाता है, आमतौर पर 64 बाइट्स। यदि आपके पास 4-बाइट पूर्णांक हैं, तो इसका मतलब है कि आप एक साफ छोटे बंडल में लगातार 16 पूर्णांक प्राप्त कर रहे हैं। यह वास्तव में स्मृति के इन टुकड़ों को लाने के लिए काफी धीमा है; आपके CPU को लोड करने के लिए एकल कैश लाइन में लगने वाले समय में बहुत काम किया जा सकता है।

अब एक्सेस के क्रम को देखें: दूसरा उदाहरण (1) 16 इनट्स का एक हिस्सा है, (2) उन सभी को संशोधित करता है, (3) 4000 * 4000/16 बार दोहराता है। यह अच्छा और तेज है, और सीपीयू में हमेशा काम करने के लिए कुछ होता है।

पहला उदाहरण है (1) 16 इनट्स का एक हिस्सा पकड़ो, (2) उनमें से केवल एक को संशोधित करें, (3) 4000 या 4000 बार दोहराएं। यह स्मृति से "भ्रूण" की संख्या के 16 गुना की आवश्यकता है। आपके सीपीयू को वास्तव में उस मेमोरी के इंतजार में बैठे हुए समय बिताना होगा, और जब वह आपके आस-पास बैठा हो, तो मूल्यवान समय बर्बाद कर रहा हो।

महत्वपूर्ण लेख:

अब आपके पास इसका जवाब है, यहाँ एक दिलचस्प बात है: इसका कोई अंतर्निहित कारण नहीं है कि आपका दूसरा उदाहरण सबसे तेज़ होना है। उदाहरण के लिए, फोरट्रान में, पहला उदाहरण तेज और दूसरा धीमा होगा। ऐसा इसलिए है क्योंकि सी की तरह वैचारिक "पंक्तियों" में चीजों का विस्तार करने के बजाय, फोरट्रान "कॉलम" में फैलता है, अर्थात:

0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3

C के लेआउट को 'रो-मेजर' और फोरट्रान को 'कॉलम-प्रमुख' कहा जाता है। जैसा कि आप देख सकते हैं, यह जानना बहुत महत्वपूर्ण है कि क्या आपकी प्रोग्रामिंग भाषा पंक्ति-प्रमुख या स्तंभ-प्रमुख है! यहाँ अधिक जानकारी के लिए एक लिंक है: http://en.wikipedia.org/wiki/Row-major_order


14
यह एक बहुत अच्छी तरह से जवाब है; यह वही है जो मुझे कैश मिस और मेमोरी मैनेजमेंट से निपटने के दौरान सिखाया गया था।
मकोतो

7
आपके पास गलत तरीके से "पहले" और "दूसरे" संस्करण हैं; पहला उदाहरण आंतरिक लूप में पहले सूचकांक को बदलता है , और धीमी निष्पादन वाला उदाहरण होगा।
कैफे

बहुत बढ़िया जवाब। अगर मार्क इस तरह के किटी किरकिरा के बारे में अधिक पढ़ना चाहते हैं, तो मैं एक किताब लिखूंगा जैसे ग्रेट कोड।
13

8
यह इंगित करने के लिए बोनस अंक कि सी ने फोरट्रान से पंक्ति क्रम को बदल दिया है। वैज्ञानिक कंप्यूटिंग के लिए L2 कैश आकार सब कुछ है क्योंकि यदि आपके सभी सरणियाँ L2 में फिट होती हैं तो मुख्य मेमोरी पर जाए बिना गणना पूरी की जा सकती है।
माइकल शोप्सिन

4
@ बैट्री: स्वतंत्र रूप से उपलब्ध हर प्रोग्रामर को मेमोरी के बारे में जानना चाहिए यह भी एक अच्छा पढ़ा गया है।
कैफे

68

विधानसभा से कोई लेना-देना नहीं। यह कैश मिस के कारण है

C बहुआयामी सरणियों को सबसे तेज़ के रूप में अंतिम आयाम के साथ संग्रहीत किया जाता है। तो पहला संस्करण हर पुनरावृत्ति पर कैश को याद करेगा, जबकि दूसरा संस्करण नहीं होगा। तो दूसरा संस्करण काफी तेजी से होना चाहिए।

यह भी देखें: http://en.wikipedia.org/wiki/Loop_interchange


23

संस्करण 2 बहुत तेजी से चलेगा क्योंकि यह आपके कंप्यूटर के कैश का संस्करण 1 से बेहतर उपयोग करता है। यदि आप इसके बारे में सोचते हैं, तो सरणियाँ स्मृति के केवल सन्निहित क्षेत्र हैं। जब आप किसी सरणी में एक तत्व का अनुरोध करते हैं, तो आपका ओएस संभवतः मेमोरी पेज को कैश में लाएगा जिसमें वह तत्व होता है। हालाँकि, चूंकि अगले कुछ तत्व उस पृष्ठ पर हैं (क्योंकि वे सन्निहित हैं), अगली पहुंच पहले से ही कैश में होगी! यह वही है जो 2 संस्करण को गति देने के लिए कर रहा है।

दूसरी ओर, संस्करण 1, तत्व कॉलम को एक्सेस कर रहा है, न कि पंक्ति-वार। इस तरह की पहुँच मेमोरी स्तर पर सन्निहित नहीं है, इसलिए प्रोग्राम OS कैशिंग का उतना फायदा नहीं उठा सकता है।


इन सरणी आकारों के साथ, शायद ओएस में बजाय सीपीयू में कैश प्रबंधक यहां जिम्मेदार है।
krlmlr

12

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


11

कैश हिट पर अन्य उत्कृष्ट उत्तरों के अलावा, एक संभावित अनुकूलन अंतर भी है। आपके दूसरे लूप को संकलक द्वारा कुछ के बराबर अनुकूलित किए जाने की संभावना है:

  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }

यह पहले लूप के लिए कम संभावना है, क्योंकि इसे हर बार 4000 के साथ सूचक "पी" को बढ़ाना होगा।

संपादित करें: p++ और यहां तक ​​कि *p++ = ..अधिकांश सीपीयू अनुदेशों में एक ही सीपीयू निर्देश के लिए संकलित किया जा सकता है। *p = ..; p += 4000नहीं, इसलिए इसे अनुकूलित करने में कम लाभ है। यह और भी कठिन है, क्योंकि कंपाइलर को आंतरिक सरणी के आकार को जानना और उसका उपयोग करना है। और ऐसा नहीं होता है कि अक्सर सामान्य कोड में आंतरिक लूप में होता है (यह केवल बहुआयामी सरणियों के लिए होता है, जहां अंतिम सूचकांक को लूप में स्थिर रखा जाता है, और दूसरे से अंतिम चरण में कदम रखा जाता है), इसलिए अनुकूलन प्राथमिकता से कम नहीं है ।


मुझे वह नहीं मिलता है, क्योंकि उसे सूचक "पी" को हर बार 4000 के साथ कूदना होगा।
विड्रैक

@Veedrac सूचक को आंतरिक लूप के अंदर 4000 के साथ बढ़ाना होगा: p += 4000isop++
fishinear

कंपाइलर को समस्या क्यों मिलेगी? iएक गैर-इकाई मूल्य से पहले से ही बढ़ा हुआ है, यह एक सूचक वेतन वृद्धि है।
विड्रैक

मैंने और अधिक स्पष्टीकरण जोड़ दिया है
मछली

Gcc.godbolt.org पर टाइप int *f(int *p) { *p++ = 10; return p; } int *g(int *p) { *p = 10; p += 4000; return p; }करने का प्रयास करें । दो मूल रूप से एक ही संकलन करने लगते हैं।
विड्रैक

7

यह रेखा अपराधी की है:

x[j][i]=i+j;

दूसरा संस्करण निरंतर मेमोरी का उपयोग करता है इस प्रकार काफी तेजी से होगा।

मैंने कोशिश की

x[50000][50000];

और संस्करण 2 के लिए संस्करण 1 बनाम 0.6 के लिए निष्पादन का समय 13s है।


4

मैं एक सामान्य जवाब देने की कोशिश करता हूं।

क्योंकि C के i[y][x]लिए एक आशुलिपि *(i + y*array_width + x)है (उत्तम दर्जे का प्रयास करें int P[3]; 0[P] = 0xBEEF;)।

जैसा कि आप पर पुनरावृति y, आप आकार के विखंडू पर पुनरावृति array_width * sizeof(array_element)। यदि आपके पास आपके आंतरिक पाश में है, तो आपके पास array_width * array_heightउन विखंडू पर पुनरावृत्तियां होंगी ।

आदेश को फ़्लिप करने से, आपके पास केवल array_heightचंक-पुनरावृत्तियाँ होंगी , और किसी भी चंक-पुनरावृत्ति के बीच, आपके पास array_widthकेवल पुनरावृत्तियों होंगे sizeof(array_element)

जबकि वास्तव में पुराने x86- सीपीयू पर यह ज्यादा मायने नहीं रखता था, आजकल 'x86 डेटा का बहुत प्रीफ़ैचिंग और कैशिंग करते हैं। आप शायद अपने धीमे चलना आदेश में कई कैश मिस का उत्पादन करते हैं

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