एक संयुक्त लूप की तुलना में अलग-अलग छोरों में एलीमेंटवाइज़ जोड़ बहुत तेज़ी से क्यों होते हैं?


2246

मान लीजिए a1, b1, c1, और d1ढेर स्मृति और मेरे संख्यात्मक कोड इंगित निम्नलिखित मुख्य लूप है।

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

इस लूप को किसी अन्य बाहरी forलूप के माध्यम से 10,000 बार निष्पादित किया जाता है । इसे गति देने के लिए, मैंने कोड को इसमें बदल दिया:

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

पूर्ण अनुकूलन के साथ MS Visual C ++ 10.0 पर संकलित और SSE2 एक इंटेल कोर 2 डुओ (x64) पर 32-बिट के लिए सक्षम है , पहला उदाहरण 5.5 सेकंड लेता है और डबल-लूप उदाहरण केवल 1.9 सेकंड लेता है। मेरा प्रश्न है: (कृपया नीचे दिए गए मेरे रेफर किए गए प्रश्न को देखें)

पुनश्च: मुझे यकीन नहीं है, अगर यह मदद करता है:

पहले लूप के लिए Disassembly मूल रूप से इस तरह दिखता है (यह ब्लॉक पूरे कार्यक्रम में लगभग पांच बार दोहराया जाता है):

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

डबल लूप उदाहरण का प्रत्येक लूप इस कोड को उत्पन्न करता है (निम्नलिखित ब्लॉक को लगभग तीन बार दोहराया जाता है):

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

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

क्या आप विवरण में कुछ ठोस जानकारी प्रदान कर सकते हैं, जो निम्न ग्राफ़ पर पांच क्षेत्रों द्वारा सचित्र अलग कैश व्यवहारों का नेतृत्व करते हैं?

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

PPS: यहाँ पूर्ण कोड है। यह उच्च रिज़ॉल्यूशन समय के लिए टीबीबी का उपयोग करता है Tick_Count, जिसे TBB_TIMINGमैक्रो को परिभाषित नहीं करके अक्षम किया जा सकता है :

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif

    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif

#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif

    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif

    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(यह विभिन्न मूल्यों के लिए FLOP / s दिखाता है n।)

यहां छवि विवरण दर्ज करें


4
ऑपरेटिंग सिस्टम हो सकता है जो भौतिक मेमोरी को खोजते समय धीमा हो जाता है जब भी आप इसे एक्सेस करते हैं और उसी मेमब्लॉक में सेकेंडरी एक्सेस के मामले में कैश की तरह कुछ होता है।
अलेक्सइयो

7
क्या आप अनुकूलन के साथ संकलन कर रहे हैं? यह O2 के लिए बहुत सारे asm कोड जैसा दिखता है ...
Luchian Grigore

1
मैंने पूछा कि कुछ समय पहले ऐसा ही सवाल क्या प्रतीत होता है । इसमें या उत्तर में ब्याज की जानकारी हो सकती है।
मार्क विल्किंस

61
बस picky होने के लिए, ये दो कोड स्निपेट संभावित ओवरलैपिंग पॉइंटर्स के कारण समतुल्य नहीं हैं। C99 में restrictऐसी स्थितियों के लिए कीवर्ड है। मुझे नहीं पता कि MSVC में ऐसा ही कुछ है। बेशक, अगर यह मुद्दा होता तो SSE कोड सही नहीं होता।
user510306

8
यह मेमोरी एलियासिंग के साथ कुछ करने के लिए हो सकता है। एक लूप के d1[j]साथ a1[j], अलाइज़ के साथ हो सकता है , इसलिए कंपाइलर कुछ मेमोरी ऑप्टिमाइज़ेशन करने से पीछे हट सकता है। जबकि ऐसा नहीं होता है यदि आप लेखन को मेमोरी में दो छोरों में अलग करते हैं।
rturrado

जवाबों:


1690

इसके आगे के विश्लेषण पर, मेरा मानना ​​है कि यह (कम से कम आंशिक रूप से) चार-बिंदुओं के डेटा संरेखण के कारण होता है। यह कैश बैंक के कुछ स्तर / कारण संघर्ष का कारण होगा।

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

इसका मतलब है कि प्रत्येक लूप में आपकी सभी पहुंच एक ही कैश तरीके से होगी। हालांकि, इंटेल प्रोसेसर में थोड़ी देर के लिए 8-रास्ता एल 1 कैश एसोसिएटिविटी रही है। लेकिन वास्तव में, प्रदर्शन पूरी तरह से समान नहीं है। 4-तरीके तक पहुंचना अभी भी 2-तरीकों की तुलना में धीमा है।

संपादित करें: यह वास्तव में ऐसा लगता है जैसे आप सभी सरणियों को अलग से आवंटित कर रहे हैं। आमतौर पर जब इस तरह के बड़े आवंटन का अनुरोध किया जाता है, तो आवंटन ओएस से नए पन्नों का अनुरोध करेगा। इसलिए, एक उच्च संभावना है कि बड़े आवंटन एक पृष्ठ-सीमा से समान ऑफसेट पर दिखाई देंगे।

यहाँ परीक्षण कोड है:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

बेंचमार्क परिणाम:

संपादित करें: एक वास्तविक कोर 2 वास्तुकला मशीन पर परिणाम :

2 x इंटेल Xeon X5482 हार्परटाउन @ 3.2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

टिप्पणियों:

  • 6.206 सेकंड एक पाश के साथ और 2.116 सेकंड दो छोरों के साथ। यह ओपी के परिणामों को बिल्कुल पुन: पेश करता है।

  • पहले दो परीक्षणों में, सरणियों को अलग से आवंटित किया जाता है। आप देखेंगे कि वे सभी पृष्ठ के सापेक्ष एक ही संरेखण हैं।

  • दूसरे दो परीक्षणों में, उस संरेखण को तोड़ने के लिए सरणियों को एक साथ पैक किया जाता है। यहाँ आप देखेंगे कि दोनों छोर तेज हैं। इसके अलावा, दूसरा (डबल) लूप अब धीमा है जैसा कि आप आमतौर पर उम्मीद करते हैं।

जैसा कि @Stephen तोप टिप्पणियों में बताती है, इस संभावना की बहुत संभावना है कि इस संरेखण के कारण लोड / स्टोर इकाइयों या कैश में झूठी अलियासिंग होती है। मैंने इसके लिए गुगली की और पाया कि इंटेल में आंशिक रूप से अलियासिंग स्टालों के लिए एक हार्डवेयर काउंटर है :

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5 क्षेत्र - स्पष्टीकरण

क्षेत्र 1:

यह एक आसान है। डेटासेट इतना छोटा है कि प्रदर्शन ओवरहेड जैसे लूपिंग और ब्रांचिंग द्वारा हावी है।

क्षेत्र 2:

यहां, जैसे-जैसे डेटा का आकार बढ़ता है, सापेक्ष ओवरहेड की मात्रा कम होती जाती है और प्रदर्शन "संतृप्त" होता है। यहां दो लूप धीमे हैं क्योंकि इसमें दो बार लूप और ब्रांचिंग ओवरहेड है।

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

क्षेत्र 3:

इस बिंदु पर, डेटा अब L1 कैश में फिट नहीं होता है। इसलिए प्रदर्शन L1 <-> L2 कैश बैंडविड्थ द्वारा कैप किया जाता है।

क्षेत्र 4:

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

हालाँकि, गलत एलाइज़िंग होने के लिए, डेटासेट के बीच पर्याप्त पर्याप्त स्ट्राइड होना चाहिए। यही कारण है कि आप इसे क्षेत्र 3 में नहीं देखते हैं।

क्षेत्र 5:

इस बिंदु पर, कुछ भी कैश में फिट नहीं होता है। तो आप मेमोरी बैंडविड्थ से बंधे हैं।


2 x इंटेल X5482 हार्परटाउन @ 3.2 गीगाहर्ट्ज़ इंटेल कोर i7 870 @ 2.8 गीगाहर्ट्ज़ Intel Core i7 2600K @ 4.4 GHz


162
+1: मुझे लगता है कि यह उत्तर है। अन्य सभी उत्तरों के विपरीत, यह एकल लूप वेरिएंट के बारे में नहीं है जो स्वाभाविक रूप से अधिक कैशे मिस करता है, यह एरे के विशेष संरेखण के बारे में है जो कैश मिस का कारण बनता है।
ओलिवर चार्ल्सवर्थ

30
इस; एक गलत अलियासिंग स्टाल सबसे अधिक संभावना स्पष्टीकरण है।
स्टीफन कैनन

7
@VictorT। मैंने ओपी से जुड़े कोड का उपयोग किया। यह एक .css फ़ाइल बनाता है जिसे मैं एक्सेल में खोल सकता हूं और उससे एक ग्राफ बना सकता हूं।
रहस्यपूर्ण

5
@ नवाज़ एक पृष्ठ आमतौर पर 4KB है। यदि आप हेक्साडेसिमल पतों को देखते हैं जो मैं प्रिंट करता हूं, तो अलग-अलग आवंटित किए गए परीक्षणों में सभी समान modulo 4096 हैं। (एक 4KB सीमा की शुरुआत से 32-बाइट्स) शायद GCC का यह व्यवहार नहीं है। यह समझा सकता है कि आप मतभेद क्यों नहीं देख रहे हैं।
रहस्यपूर्ण निर्णय


224

ठीक है, सही जवाब निश्चित रूप से सीपीयू कैश के साथ कुछ करना है। लेकिन कैश तर्क का उपयोग करने के लिए काफी मुश्किल हो सकता है, खासकर डेटा के बिना।

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

@ रहस्यवादी के उत्तर ने बहुत से लोगों (मेरे सहित) को आश्वस्त किया, शायद इसलिए कि यह एकमात्र ऐसा था जो तथ्यों पर भरोसा करता था, लेकिन यह सच्चाई का केवल एक "डेटा बिंदु" था।

यही कारण है कि मैंने उनके परीक्षण (एक निरंतर बनाम अलग आवंटन का उपयोग करके) और @ जेम्स उत्तरदाता की सलाह को संयुक्त किया।

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

ध्यान दें कि मेरा प्रारंभिक प्रश्न n = 100.000 पर था । यह बिंदु (दुर्घटना से) विशेष व्यवहार प्रदर्शित करता है:

  1. यह एक और दो लूप संस्करण के बीच सबसे बड़ी विसंगति रखता है (लगभग तीन का एक कारक)

  2. यह एकमात्र बिंदु है, जहां एक-लूप (निरंतर आवंटन के साथ) दो-लूप संस्करण को हरा देता है। (इसने मिस्टिकल का जवाब संभव बना दिया है।)

आरंभिक डेटा का उपयोग करके परिणाम:

यहां छवि विवरण दर्ज करें

अनइंस्टॉल किए गए डेटा का उपयोग करने का परिणाम (यह वही है जो मिस्टिक परीक्षण किया गया है):

यहां छवि विवरण दर्ज करें

और यह एक कठिन व्याख्या है: प्रारंभिक डेटा, जिसे एक बार आवंटित किया जाता है और विभिन्न वेक्टर के प्रत्येक निम्नलिखित परीक्षण मामले के लिए पुन: उपयोग किया जाता है:

यहां छवि विवरण दर्ज करें

प्रस्ताव

स्टैक ओवरफ्लो पर हर निम्न-स्तरीय प्रदर्शन से संबंधित प्रश्न को कैश प्रासंगिक डेटा आकारों की पूरी श्रृंखला के लिए MFLOPS जानकारी प्रदान करने की आवश्यकता होनी चाहिए! उत्तरों के बारे में सोचना और विशेष रूप से इस जानकारी के बिना दूसरों के साथ चर्चा करना हर किसी के समय की बर्बादी है।


18
+1 अच्छा विश्लेषण। मैंने पहले स्थान पर डेटा को अनधिकृत रूप से छोड़ने का इरादा नहीं किया था। यह सिर्फ इतना हुआ कि आवंटनकर्ता ने उन्हें वैसे भी शून्य कर दिया। तो आरंभिक डेटा क्या मायने रखता है। मैंने सिर्फ एक वास्तविक कोर 2 आर्किटेक्चर मशीन पर परिणामों के साथ अपना उत्तर संपादित किया और वे उस चीज़ के बहुत करीब हैं जो आप देख रहे हैं। एक और बात यह है कि मैंने कई आकारों का परीक्षण किया nऔर यह उसी प्रदर्शन के अंतर को दिखाता है n = 80000, n = 100000, n = 200000, आदि ...
रहस्यवादी

2
@Mysticial मुझे लगता है कि जब भी संभव अंतर प्रक्रिया जासूसी से बचने के लिए नए पेज एक प्रक्रिया को देते हुए OS को लागू करता है, तो पृष्ठ को शून्य करता है।
v.oddou

1
@ v.oddou: व्यवहार OS पर भी निर्भर करता है; IIRC, विंडोज़ में शून्य-आउट फ़्रीड पृष्ठों को पृष्ठभूमि में रखने के लिए एक थ्रेड है, और यदि अनुरोध पहले से ही शून्य पृष्ठों से संतुष्ट नहीं किया जा सकता है, तो VirtualAllocकॉल तब तक ब्लॉक करता है जब तक कि अनुरोध को पूरा करने के लिए पर्याप्त शून्य न हो। इसके विपरीत, लिनक्स बस शून्य पृष्ठ को कॉपी-ऑन-राइट के रूप में आवश्यक के रूप में मैप करता है, और लिखने पर, यह नए डेटा में लिखने से पहले नए शून्य को एक नए पृष्ठ पर कॉपी करता है। किसी भी तरह से, उपयोगकर्ता मोड प्रक्रिया के दृष्टिकोण से, पृष्ठों को शून्य कर दिया जाता है, लेकिन असंबद्ध मेमोरी का पहला उपयोग आमतौर पर विंडोज पर लिनक्स की तुलना में अधिक महंगा होगा।
शैडो रेंजर

81

दूसरे लूप में बहुत कम कैश गतिविधि शामिल है, इसलिए प्रोसेसर के लिए मेमोरी मांगों के साथ रखना आसान है।


1
आप कह रहे हैं कि दूसरा संस्करण कम कैश की कमी को पूरा करता है? क्यों?
ओलिवर चार्ल्सवर्थ

2
@Oli: पहले संस्करण का में, प्रोसेसर का उपयोग चार स्मृति लाइनों के लिए एक समय में की जरूरत है a[i], b[i], c[i]और d[i]दूसरे संस्करण में, यह सिर्फ दो की जरूरत है। यह जोड़ते समय उन लाइनों को फिर से भरने के लिए बहुत अधिक व्यवहार्य बनाता है।
पिल्ला

4
लेकिन जब तक एरे कैश में टकराते नहीं हैं, तब तक प्रत्येक वेरिएंट को ठीक उसी संख्या में रीड की आवश्यकता होती है और / से लेकर मुख्य मेमोरी तक लिखता है। तो निष्कर्ष यह है कि (मुझे लगता है) कि ये दो सरणियाँ हर समय टकराती रहती हैं।
ओलिवर चार्ल्सवर्थ

3
मैं अनुसरण नहीं करता। निर्देश (प्रति उदाहरण x += y), दो रीड और एक लेखन हैं। यह या तो संस्करण के लिए सच है। कैश <-> सीपीयू बैंडविड्थ की आवश्यकता इसलिए समान है। इसलिए जब तक कोई संघर्ष नहीं होता है, कैश <-> रैम बैंडविड्थ की आवश्यकता भी समान होती है ..
ओलिवर चार्ल्सवर्थ

2
जैसा कि stackoverflow.com/a/1742231/102916 में लिखा गया है , पेंटियम एम का हार्डवेयर प्रीफ़ैच 12 अलग-अलग फ़ॉरवर्ड स्ट्रीम ट्रैक कर सकता है (और मुझे उम्मीद है कि बाद में हार्डवेयर कम से कम सक्षम होगा)। लूप 2 अभी भी केवल चार धाराओं को पढ़ रहा है, इसलिए उस सीमा के भीतर अच्छी तरह से है।
ब्रूक्स मूसा

50

कल्पना कीजिए कि आप एक ऐसी मशीन पर काम कर रहे हैं, जहाँ nएक समय में मेमोरी में अपने दो सरणियों को धारण करना संभव है, लेकिन डिस्क कैशिंग के माध्यम से उपलब्ध कुल मेमोरी अभी भी चारों को धारण करने के लिए पर्याप्त है।

एक साधारण LIFO कैशिंग नीति, इस कोड को मानते हुए:

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

पहले कारण होगा aऔर bरैम में लोड किया जाएगा और फिर पूरी तरह से रैम में काम किया जाएगा। जब दूसरा लूप शुरू होता है, cऔर dफिर डिस्क से रैम में लोड किया जाता है और चालू होता है।

दूसरा लूप

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

लूप के चारों ओर हर बार दो अन्य में पेज और दो अन्य पेजों को पेज आउट करेंगे । यह स्पष्ट रूप से बहुत धीमा होगा।

आप शायद अपने परीक्षणों में डिस्क कैशिंग नहीं देख रहे हैं, लेकिन आप शायद कैशिंग के कुछ अन्य रूप के दुष्प्रभाव देख रहे हैं।


यहाँ थोड़ा भ्रम / गलतफहमी होने लगती है इसलिए मैं एक उदाहरण का उपयोग करके थोड़ा विस्तार करने की कोशिश करूँगा।

कहो n = 2और हम बाइट्स के साथ काम कर रहे हैं। इस प्रकार हमारे परिदृश्य में हमारे पास सिर्फ 4 बाइट्स रैम है और हमारी बाकी मेमोरी काफी धीमी है (100 गुना लंबी पहुंच का कहना है)।

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

  • साथ में

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
    
  • कैश a[0]और a[1]तो b[0]और b[1]और सेट a[0] = a[0] + b[0]कैश में - वहाँ अब कैश में चार बाइट्स हैं, a[0], a[1]और b[0], b[1]। लागत = 100 + 100।

  • a[1] = a[1] + b[1]कैश में सेट करें। लागत = 1 + 1।
  • के लिए दोहराएँ cऔर d
  • कुल लागत = (100 + 100 + 1 + 1) * 2 = 404

  • साथ में

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
    
  • कैश a[0]और a[1]तो b[0]और b[1]और सेट a[0] = a[0] + b[0]कैश में - वहाँ अब कैश में चार बाइट्स हैं, a[0], a[1]और b[0], b[1]। लागत = 100 + 100।

  • a[0], a[1], b[0], b[1]कैश और कैश से बेदखल करना c[0]और c[1]फिर d[0]और कैश में d[1]सेट c[0] = c[0] + d[0]करना। लागत = 100 + 100।
  • मुझे संदेह है कि आप यह देखने लगे हैं कि मैं कहां जा रहा हूं।
  • कुल लागत = (100 + 100 + 100 + 100) * 2 = 800

यह एक क्लासिक कैश थ्रैश परिदृश्य है।


12
यह गलत है। किसी सरणी के किसी विशेष तत्व के संदर्भ में डिस्क से (या गैर-कैश्ड मेमोरी से) पूरे सरणी को पृष्ठांकित नहीं किया जा सकता है; केवल संबंधित पृष्ठ या कैश लाइन में पेज किया गया है।
ब्रूक्स मूसा

1
@ ब्रूक्स मूसा - यदि आप पूरे सरणी से चलते हैं, जैसा कि यहां हो रहा है, तो यह होगा।
ओल्डकर्मुडीनगर

1
ठीक है, हाँ, लेकिन पूरे ऑपरेशन में यही होता है, ना कि लूप के आसपास हर बार क्या होता है। आपने दावा किया कि दूसरा रूप "लूप के चारों ओर हर दो समय में दो सरणियों और पृष्ठ को बाहर करेगा," और यही वह है जिस पर मुझे आपत्ति है। समग्र सरणियों के आकार के बावजूद, इस लूप के मध्य में आपकी रैम चार सरणियों में से प्रत्येक से एक पृष्ठ पकड़े हुए होगी, और लूप के समाप्त होने के बाद तक कुछ भी बाहर नहीं मिलेगा।
ब्रूक्स मूसा

उस विशेष मामले में जहां n केवल इसके लिए सही मान था, एक समय में स्मृति में अपने दो सरणियों को पकड़ना संभव हो सकता है, तो एक लूप में चार सरणियों के सभी तत्वों तक पहुंच को निश्चित रूप से थ्रशिंग समाप्त होना चाहिए।
OldCurmudgeon

1
आप उनमें से प्रत्येक के पहले पृष्ठ के बजाय पूरे a1और पूरे b1असाइनमेंट के लिए उस लूप 2 पेज पर क्यों रह रहे हैं? (क्या आप 5-बाइट पृष्ठ मान रहे हैं, इसलिए एक पृष्ठ आपके रैम का आधा है! यह सिर्फ स्केलिंग नहीं है, यह पूरी तरह से एक वास्तविक प्रोसेसर है।)
ब्रूक्स मूसा

35

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

पहला कोड प्रत्येक लूप में उन्हें वैकल्पिक मेमोरी एड्रेस को संशोधित करता है, इस प्रकार कैश को अमान्य करने के लिए निरंतर आवश्यकता होती है।

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


इसके कारण कैश को लगातार अमान्य क्यों किया जाएगा?
ओलिवर चार्ल्सवर्थ

1
@OliCharlesworth: मेमोरी पतों की एक सन्निहित सीमा की हार्ड कॉपी के रूप में कैश के बारे में सोचें। यदि आप किसी पते का उपयोग नहीं करने का दिखावा करते हैं, तो आपको कैश को फिर से लोड करना होगा। और अगर कैश में कुछ संशोधित किया गया था, तो इसे रैम में वापस लिखना होगा, या यह खो जाएगा। नमूना कोड में, 100'000 पूर्णांक (400kBytes) के 4 वेक्टर एल 1 कैश (128 या 256K) की क्षमता से अधिक होने की संभावना है।
एमिलियो गारवागलिया

5
कैश का आकार इस परिदृश्य में कोई प्रभाव नहीं है। प्रत्येक सरणी तत्व का उपयोग केवल एक बार किया जाता है, और उसके बाद यह बेदखल होने पर कोई फर्क नहीं पड़ता। कैश आकार केवल तभी मायने रखता है जब आपके पास टेम्पोरल लोकलिटी हो (यानी आप भविष्य में उसी तत्वों का फिर से उपयोग करने जा रहे हों)।
ओलिवर चार्ल्सवर्थ

2
@OliCharlesworth: यदि मुझे कैश में एक नया मान लोड करना है, और इसमें पहले से ही एक मूल्य है जिसे संशोधित कर दिया गया है, तो मुझे इसे सबसे पहले लिखना होगा, और इससे मुझे लिखने के लिए प्रतीक्षा करनी होगी।
एमिलियो गरवाग्लिया

2
लेकिन ओपी कोड के दोनों वेरिएंट में, प्रत्येक मूल्य एक बार ठीक से संशोधित हो जाता है। आप प्रत्येक संस्करण में एक ही संख्या में राइट-बैक करते हैं।
ओलिवर चार्ल्सवर्थ

22

मैं यहां चर्चा किए गए परिणामों को दोहरा नहीं सकता।

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

आठ छोरों का उपयोग करके सरणी आकार 2 ^ 16 से 2 ^ 24 तक था। मैं स्रोत सरणियों को शुरू करने के लिए सावधान था, इसलिए +=असाइनमेंट एफपीयू को एक डबल के रूप में व्याख्या किए गए मेमोरी कचरे को जोड़ने के लिए नहीं कह रहा था ।

मैं इस तरह के काम के रूप में डाल विभिन्न योजनाओं, के साथ चारों ओर खेला b[j], d[j]के InitToZero[j]छोरों के अंदर, और भी उपयोग करने के साथ += b[j] = 1और += d[j] = 1, और मैं काफी संगत परिणाम मिला है।

आप, उम्मीद कर सकते हैं आरंभ bऔर dपाश का उपयोग कर अंदर InitToZero[j], संयुक्त दृष्टिकोण फायदा देते थे के रूप में वे करने के लिए कार्य करने से पहले बैक-टू-बैक किया गया था aऔर c, लेकिन अभी भी भीतर 10%। जाओ पता लगाओ।

हार्डवेयर डेल एक्सपीएस 8500 पीढ़ी 3 कोर i7 @ 3.4 गीगाहर्ट्ज और 8 जीबी मेमोरी के साथ है। 8 छोरों का उपयोग करते हुए 2 ^ 16 से 2 ^ 24 के लिए, संचयी समय क्रमशः 44.987 और 40.965 था। दृश्य C ++ 2010, पूरी तरह से अनुकूलित।

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

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

मुझे यकीन नहीं है कि यह क्यों तय किया गया था कि एमएफएलओपीएस एक प्रासंगिक मीट्रिक था। हालांकि मुझे यह विचार मेमोरी एक्सेस पर ध्यान केंद्रित करने के लिए था, इसलिए मैंने फ्लोटिंग पॉइंट कम्प्यूटेशन समय की मात्रा को कम करने की कोशिश की। मैंने अंदर छोड़ दिया +=, लेकिन मुझे यकीन नहीं है कि क्यों।

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


1
आपके द्वारा उल्लेखित मिसलिग्न्मेंट पेनल्टी उस समय होती है जब किसी व्यक्ति का लोड / स्टोर होता है जिसे मिसल किया जाता है (जिसमें अलिखित एसएसई लोड / स्टोर शामिल होता है)। लेकिन यह अलग-अलग सरणियों के सापेक्ष संरेखण के प्रति संवेदनशील होने के कारण यहाँ ऐसा नहीं है। निर्देश स्तर पर कोई भी मिसलिग्न्मेंट नहीं हैं। हर एक लोड / स्टोर ठीक से गठबंधन किया गया है।
मि।

18

ऐसा इसलिए है क्योंकि सीपीयू में बहुत सारे कैश मिस नहीं हैं (जहां उसे रैम चिप्स से आने के लिए एरे डेटा का इंतजार करना पड़ता है)। आपके लिए यह दिलचस्प होगा कि आप सरणियों के आकार को लगातार समायोजित करें ताकि आप अपने सीपीयू के स्तर 1 कैश (L1), और फिर स्तर 2 कैश (L2) के आकार से अधिक हो जाएं और अपने कोड के लिए लिया गया समय प्लॉट करें सरणियों के आकार के खिलाफ निष्पादित करने के लिए। ग्राफ एक सीधी रेखा में नहीं होना चाहिए, जैसे आप अपेक्षा करते हैं।


2
मुझे विश्वास नहीं है कि कैश आकार और सरणी आकार के बीच कोई इंटरैक्शन है। प्रत्येक सरणी तत्व केवल एक बार उपयोग किया जाता है, और फिर सुरक्षित रूप से निकाला जा सकता है। कैश लाइन आकार और सरणी आकार के बीच एक अंतःक्रिया हो सकती है , हालांकि, अगर यह चार सरणियों को संघर्ष का कारण बनता है।
ओलिवर चार्ल्सवर्थ

15

प्रत्येक लूप में पहला लूप वैकल्पिक लिखता है। दूसरे और तीसरे वाले केवल तत्व आकार के छोटे जंप बनाते हैं।

20 क्रॉस की दो समानांतर पंक्तियों को एक पेन और पेपर के साथ लिखने की कोशिश करें जो 20 सेमी से अलग हो। एक बार और फिर दूसरी लाइन को पूरा करने की कोशिश करें और हर लाइन में एक क्रॉस को वैकल्पिक रूप से लिखकर दूसरी बार कोशिश करें।


सीपीयू के निर्देश जैसी चीजों के बारे में सोचने पर वास्तविक दुनिया की गतिविधियों के अनुरूप संकट से घिर जाते हैं। आप जो दर्शा रहे हैं वह प्रभावी रूप से समय की तलाश है , जो कि अगर हम कताई डिस्क पर संग्रहीत डेटा को पढ़ने / लिखने के बारे में बात कर रहे हैं, तो लागू होगा, लेकिन सीपीयू कैश (या रैम, या एसएसडी) में कोई समय नहीं है। मेमोरी के क्षेत्रों को अलग करने के लिए पहुँच कोई जुर्माना बनाम आसन्न पहुँच नहीं है।
FeRD

7

मूल प्रश्न

एक पाश दो छोरों की तुलना में इतना धीमा क्यों है?


निष्कर्ष:

केस 1 एक क्लासिक इंटरपोलेशन समस्या है जो एक अक्षम होने के लिए होती है। मुझे यह भी लगता है कि यह एक प्रमुख कारण था कि कई मशीन आर्किटेक्चर और डेवलपर्स ने मल्टी-थ्रेडेड एप्लिकेशन के साथ-साथ समानांतर प्रोग्रामिंग करने की क्षमता के साथ मल्टी-कोर सिस्टम का निर्माण और डिजाइनिंग को समाप्त किया।

हार्डवेयर, ओएस, और कंपाइलर (ओं) को एक साथ काम करने के लिए इस तरह के दृष्टिकोण से इसे देखने के लिए एक साथ ढेर आवंटन करने के लिए काम करता है जिसमें रैम, कैश, पेज फ़ाइलें, आदि के साथ काम करना शामिल है; गणित जो इन एल्गोरिदम की नींव में है, हमें दिखाता है कि इन दोनों में से कौन सा बेहतर समाधान है।

हम एक के एक सादृश्य का उपयोग कर सकते Bossएक किया जा रहा Summationहै कि एक का प्रतिनिधित्व करेंगी For Loopकार्यकर्ताओं के बीच यात्रा नहीं है Aऔर B

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


अब मैं यह समझाना शुरू करूंगा कि यह सब नीचे कैसे काम करता है।


समस्या का आकलन

ओपी का कोड:

const int n=100000;

for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

तथा

for(int j=0;j<n;j++){
    a1[j] += b1[j];
}
for(int j=0;j<n;j++){
    c1[j] += d1[j];
}

विचार

ओपी के मूल प्रश्न को लूप के 2 वेरिएंट के बारे में ध्यान में रखते हुए और अन्य कई उत्कृष्ट उत्तरों और उपयोगी टिप्पणियों के साथ-साथ कैश के व्यवहार के प्रति उनका संशोधित प्रश्न; मैं इस स्थिति और समस्या के बारे में एक अलग दृष्टिकोण लेकर यहाँ कुछ अलग करने की कोशिश करना चाहता हूँ।


पहुंच

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


परिदृश्य

थोड़ी देर के लिए कोड को देखने के बाद यह काफी स्पष्ट हो गया कि समस्या क्या है और यह क्या पैदा कर रहा है। आइए इसे एक एल्गोरिथम समस्या में तोड़ते हैं और इसे गणितीय अंकन का उपयोग करने के दृष्टिकोण से देखते हैं और फिर गणित की समस्याओं के साथ-साथ एल्गोरिदम पर भी लागू होते हैं।


हम क्या जानते हैं

हम जानते हैं कि यह लूप 100,000 बार चलेगा। हम यह भी जानते हैं कि a1, b1, c1और d1एक 64-बिट वास्तुकला पर संकेत दिए गए हैं। 32-बिट मशीन पर C ++ के भीतर, सभी पॉइंटर्स 4 बाइट्स हैं और 64-बिट मशीन पर, वे 8 बाइट्स आकार में हैं क्योंकि पॉइंटर्स एक निश्चित लंबाई के हैं।

हम जानते हैं कि हमारे पास 32 बाइट्स हैं, जिसमें दोनों मामलों के लिए आवंटन करना है। एकमात्र अंतर यह है कि हम प्रत्येक पुनरावृत्ति पर 32 बाइट्स या 2-8 बाइट्स के 2 सेट आवंटित कर रहे हैं जिसमें दूसरा मामला है जो हम दोनों स्वतंत्र लूप के लिए प्रत्येक पुनरावृत्ति के लिए 16 बाइट्स आवंटित कर रहे हैं।

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

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


जो हम नहीं जानते

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


जांच करते हैं

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


हमारे दावे:

  • हम अपने लूप और उसके पुनरावृत्तियों को 1 पर शुरू होने वाले योग के रूप में देंगे और 0 के साथ शुरू होने के बजाय 100000 पर समाप्त होता है क्योंकि लूप्स में हमें 0 एड्रेसिंग स्कीम के बारे में चिंता करने की आवश्यकता नहीं है क्योंकि हम सिर्फ रुचि रखते हैं। एल्गोरिथ्म ही।
  • दोनों मामलों में हमारे पास 4 कार्य हैं और 2 फ़ंक्शन कॉल के साथ 2 फ़ंक्शन प्रत्येक फ़ंक्शन कॉल पर किए जा रहे हैं। हम निम्नलिखित के रूप में कार्य करने के लिए कार्य करता है और कॉल के रूप में इन स्थापित करेगा: F1(), F2(), f(a), f(b), f(c)और f(d)

एल्गोरिदम:

पहला मामला: - केवल एक समन लेकिन दो स्वतंत्र फ़ंक्शन कॉल।

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a) + f(b); }
                       F2() = { f(c) = f(c) + f(d); }

दूसरा मामला: - दो योग लेकिन प्रत्येक का अपना फ़ंक्शन कॉल है।

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a) + f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c) + f(d); }

तो आपने देखा F2()ही में मौजूद है Sumसे Case1जहां F1()में निहित है Sumसे Case1और दोनों में Sum1और Sum2से Case2। यह बाद में स्पष्ट होगा जब हम निष्कर्ष निकालना शुरू करते हैं कि एक अनुकूलन है जो दूसरे एल्गोरिथ्म के भीतर हो रहा है।

पहले केस Sumकॉल्स के माध्यम से पुनरावृत्तियों f(a)जो अपने आप को जोड़ देगा, f(b)फिर वह कॉल करता है f(c)जो ऐसा ही करेगा लेकिन f(d)प्रत्येक 100000पुनरावृत्तियों के लिए खुद को जोड़ देगा । दूसरे मामले में, हमारे पास Sum1और Sum2यह है कि दोनों एक ही कार्य करते हैं मानो वे एक ही कार्य पंक्ति में दो बार बुलाए जा रहे हों।

इस मामले में हम इलाज कर सकते हैं Sum1और Sum2सिर्फ सादे पुराने Sumजहां Sumइस मामले में इस तरह दिखता है: Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }और अब यह एक अनुकूलन की तरह दिखता है जहां हम इसे केवल एक ही कार्य मान सकते हैं।


सादृश्य के साथ सारांश

दूसरे मामले में हमने जो देखा है, वह लगभग ऐसा प्रतीत होता है जैसे कि अनुकूलन है क्योंकि दोनों छोरों के लिए समान सटीक हस्ताक्षर हैं, लेकिन यह वास्तविक मुद्दा नहीं है। मुद्दा काम कर रहा है कि द्वारा किया जा रहा नहीं है f(a), f(b), f(c), और f(d)। दोनों मामलों में और दोनों के बीच की तुलना में, यह उस अंतर का अंतर है जो योग के प्रत्येक मामले में यात्रा करना है जो आपको निष्पादन के समय में अंतर देता है।

के बारे में सोचो For Loopsहोने के रूप में Summationsहै कि एक होने के रूप में पुनरावृत्तियों करता है Bossकि दो लोगों के लिए आदेश दे रहा है Aऔर Bऔर कहा कि अपनी नौकरी मांस के लिए कर रहे Cऔर Dक्रमश: और उनमें से कुछ पैकेज लेने और इसे वापस करने के लिए। इस सादृश्य में, लूप्स या समिश्र पुनरावृत्तियों और स्थिति की जाँच के लिए स्वयं वास्तव में प्रतिनिधित्व नहीं करते हैं Boss। क्या वास्तव में प्रतिनिधित्व Bossवास्तविक गणितीय एल्गोरिदम सीधे से लेकिन की वास्तविक अवधारणा से नहीं है Scopeऔर Code Blockभीतर एक नियमित या सबरूटीन, विधि, समारोह, अनुवाद इकाई, आदि पहले एल्गोरिथ्म 1 गुंजाइश जहां 2 एल्गोरिथ्म लगातार 2 स्कोप है है।

प्रत्येक कॉल पर्ची पर पहले मामले के भीतर, Bossको जाता है Aऔर आदेश देता है और Aबंद हो जाता है लाने के लिए B'sतो पैकेज Bossको जाता है Cऔर आदेश देता है एक ही है और से पैकेज प्राप्त करने के लिए Dप्रत्येक यात्रा पर।

दूसरे मामले में, सभी पैकेज प्राप्त होने तक पैकेज को Bossसीधे Aजाने और लाने के साथ काम करता है B's। फिर सभी पैकेज प्राप्त करने के लिए उसी के Bossसाथ काम करता है।CD's

चूंकि हम 8-बाइट पॉइंटर के साथ काम कर रहे हैं और हीप आवंटन से निपटने के लिए निम्नलिखित समस्या पर विचार करते हैं। मान लीजिए कि Boss100 फीट से है Aऔर वह A500 फीट से है C। हमें इस बारे में चिंता करने की आवश्यकता नहीं है कि निष्पादन के आदेश के कारण Bossशुरू से कितनी दूर है C। दोनों मामलों में, Bossशुरू में Aपहले से फिर यात्रा होती है B। यह सादृश्य यह कहने के लिए नहीं है कि यह दूरी सटीक है; यह एल्गोरिदम के कामकाज को दिखाने के लिए सिर्फ एक उपयोगी परीक्षण परिदृश्य है।

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


टेस्ट मामलों:

पहला मामला: पहली यात्रा परBossशुरू में 100 फीट जाना करने के लिए पर्ची देने के लिए हैAऔरAबंद हो जाता है और उसकी बात करता है, लेकिन उसके बादBoss500 फीट यात्रा करने के लिए किया हैCउसे अपने आदेश पर्ची देने के लिए। फिर अगले पुनरावृत्ति और हर दूसरे पुनरावृत्ति के बादBossदोनों के बीच 500 फीट आगे और पीछे जाना है।

दूसरा मामला:Boss करने के लिए पहली यात्रा पर 100 फीट यात्रा करने के लिए हैA, लेकिन उसके बाद, वह पहले से ही है और बस के लिए इंतजार कर रहा हैAवापस पाने के लिए जब तक सभी स्लिप भर रहे हैं। फिरBossपहले यात्रा पर 500 फीट की यात्रा करनी होती हैCक्योंकिC500 फीट से हैA। चूँकिउसकेBoss( Summation, For Loop )साथ काम करने के बाद इसे सही कहा जाAरहा है, इसलिए वह वहीं इंतजार करता है जैसा उसनेAतब तककिया थाजब तक कि सभीC'sऑर्डर स्लिप नहीं हो जाते।


अंतर यात्रा में अंतर

const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500); 
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst =  10000500;
// Distance Traveled On First Algorithm = 10,000,500ft

distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;    

तुलनात्मक मूल्यों की तुलना

हम आसानी से देख सकते हैं कि 600 10 मिलियन से कम है। अब, यह सटीक नहीं है, क्योंकि हम राम के पते के बीच की दूरी या जिसमें से कैश या पेज फ़ाइल के प्रत्येक अंतर पर प्रत्येक कॉल को वास्तविक अंतर नहीं पता है, कई अन्य अनदेखी चर के कारण होने जा रहा है। यह स्थिति के बारे में जागरूक होने और इसे सबसे खराब स्थिति से देखने का सिर्फ एक आकलन है।

इन नंबरों से यह लगभग ऐसा प्रतीत होगा जैसे कि अल्गोरिथम वन 99%अल्गोरिथम टू की तुलना में धीमा होना चाहिए ; हालांकि, यह केवल है Boss'sहिस्सा है या एल्गोरिदम की जिम्मेदारी है और यह वास्तविक श्रमिकों के लिए खाते में नहीं है A, B, C, और Dऔर क्या वे एक और लूप के हर यात्रा पर क्या करना है। इसलिए बॉस की नौकरी में कुल काम का लगभग 15 - 40% हिस्सा होता है। श्रमिकों के माध्यम से जो काम किया जाता है उसका बहुत बड़ा प्रभाव पड़ता है, गति दर के अंतर को लगभग 50-70% तक रखने का


अवलोकन: - दो एल्गोरिदम के बीच अंतर

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

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

यह उन ASMनिर्देशों के साक्ष्य से अवलोकनीय है जो दोनों मामलों में दिखाए गए थे। क्या पहले से ही इन मामलों के बारे में कहा गया था के साथ, इस तथ्य यह है कि में के लिए खाते में नहीं है केस 1 मालिक दोनों के लिए प्रतीक्षा करनी होगी Aऔर Cवापस पाने के लिए इससे पहले कि वह वापस लिए जा सकते हैं Aप्रत्येक यात्रा के लिए फिर से। यह भी सच है कि अगर के लिए खाते में नहीं है Aया Bतो एक बहुत ही समय लग रहा है दोनों Bossऔर अन्य कार्यकर्ता (रों) निष्क्रिय प्रतीक्षा निष्पादित करने के लिए कर रहे हैं।

में केस 2 केवल एक ही जा रहा है बेकार है Bossजब तक कार्यकर्ता वापस हो जाता है। तो यह भी एल्गोरिथ्म पर प्रभाव पड़ता है।



ओपी संशोधित प्रश्न

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

क्या आप विवरण में कुछ ठोस जानकारी प्रदान कर सकते हैं, जो निम्न ग्राफ़ पर पांच क्षेत्रों द्वारा सचित्र अलग कैश व्यवहारों का नेतृत्व करते हैं?

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


इन सवालों के बारे में

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

अब मेमोरी और कैशिंग के साथ-साथ पेज फाइल आदि के प्रबंधन के लिए, जो सभी निम्नलिखित के बीच सिस्टम के एक एकीकृत सेट में एक साथ काम करते हैं:

  • The Architecture {हार्डवेयर, फ़र्मवेयर, कुछ एंबेडेड ड्राइवर, कर्नेल और एएसएम इंस्ट्रक्शन सेट्स}।
  • The OS{फ़ाइल और मेमोरी प्रबंधन प्रणाली, ड्राइवर और रजिस्ट्री}।
  • The Compiler {अनुवाद इकाइयाँ और स्रोत कोड के अनुकूलन}।
  • और यहां तक ​​कि Source Codeविशिष्ट एल्गोरिदम के अपने सेट (ओं) के साथ भी ।

हम पहले से ही देख सकते हैं एक टोंटी है कि पहले एल्गोरिथ्म के भीतर क्या हो रहा है इससे पहले कि हम भी किसी भी मनमाने ढंग से साथ किसी भी मशीन पर लागू है कि वहाँ Architecture, OSहै, और Programmable Languageदूसरा एल्गोरिथ्म की तुलना में। आधुनिक कंप्यूटर के आंतरिक भाग को शामिल करने से पहले ही एक समस्या थी।


अंतिम परिणाम

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

यदि आपने Bossऔर दो श्रमिकों की सादृश्य पर ध्यान दिया Aऔर Bजिन्हें जाना था Cऔर Dक्रमशः और दो एल्गोरिदम के गणितीय संकेतन पर विचार करके पैकेज प्राप्त करना था ; आप कंप्यूटर हार्डवेयर और सॉफ्टवेयर की भागीदारी के बिना देख सकते हैं की तुलना में Case 2लगभग 60%तेज है Case 1

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

अगर Dataसेट काफी छोटा है, तो यह सब एक अंतर का बुरा नहीं लग सकता है। हालांकि, चूंकि हम समय के निष्पादन में अंतर के मामले में इस फ़ंक्शन की वृद्धि को देखने की तुलना Case 1में 60 - 70%धीमी हैं।Case 2

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where 
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with 
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

यह अनुमान सॉफ्टवेयर अनुकूलन और मशीन निर्देशों को शामिल करते हुए एल्गोरिदम और मशीन संचालन दोनों इन दो छोरों के बीच औसत अंतर है।

जब डेटा सेट रैखिक रूप से बढ़ता है, तो दोनों के बीच के समय में अंतर होता है। जब एल्गोरिथ्म 1 एल्गोरिथ्म 2 की तुलना में अधिक फ़ेच जो स्पष्ट है है Bossके बीच यात्रा आगे और पीछे अधिकतम दूरी के लिए है Aऔर Cपहली यात्रा के बाद के लिए हर यात्रा, जबकि एल्गोरिथ्म 2 Bossहै के लिए यात्रा करने के लिए Aएक बार और फिर से किया जा रहा है के बाद Aवह यात्रा करने के लिए है अधिकतम दूरी केवल एक बार जब से जा रही Aहो C

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



संशोधन: सॉफ्टवेयर इंजीनियरिंग डिजाइन सिद्धांत

- छोरों के लिए पुनरावृत्तियों के भीतर Local Stackऔर Heap Allocatedअभिकलन के बीच का अंतर और उनके उपयोग, उनकी प्रभावकारिता और प्रभावशीलता के बीच अंतर - और

गणितीय एल्गोरिथ्म जो मैंने ऊपर प्रस्तावित किया था, मुख्य रूप से उन छोरों पर लागू होता है जो ढेर पर आवंटित डेटा पर संचालन करते हैं।

  • लगातार ढेर संचालन:
    • यदि लूप एकल कोड ब्लॉक या स्कोप के भीतर स्थानीय रूप से डेटा पर संचालन कर रहे हैं जो स्टैक फ्रेम के भीतर है तो यह अभी भी लागू होगा, लेकिन मेमोरी लोकेशन बहुत करीब हैं जहां वे आमतौर पर अनुक्रमिक हैं और दूरी की यात्रा या निष्पादन समय में अंतर है। लगभग नगण्य है। चूंकि ढेर के भीतर कोई आवंटन नहीं किया जा रहा है, मेमोरी बिखरी हुई नहीं है, और मेमोरी को राम के माध्यम से नहीं लाया जा रहा है। मेमोरी आमतौर पर स्टैक फ्रेम और स्टैक पॉइंटर के सापेक्ष अनुक्रमिक और सापेक्ष होती है।
    • जब स्टैक पर लगातार संचालन किया जा रहा है, तो एक आधुनिक प्रोसेसर स्थानीय कैश रजिस्टर में इन मूल्यों को रखते हुए दोहराए जाने वाले मूल्यों और पते को कैश करेगा। यहां संचालन या निर्देशों का समय नैनो-सेकंड के क्रम पर है।
  • लगातार ढेर आवंटित संचालन:
    • जब आप ढेर आवंटन लागू करना शुरू करते हैं और प्रोसेसर को सीपीयू, बस कंट्रोलर और राम मॉड्यूल की वास्तुकला के आधार पर मेमोरी कॉल्स को लगातार कॉल पर लाना पड़ता है, तो ऑपरेशन के समय या निष्पादन माइक्रो के आदेश पर हो सकते हैं मिलीसेकेंड। कैश्ड स्टैक संचालन की तुलना में, ये काफी धीमी हैं।
    • सीपीयू को राम से मेमोरी पता प्राप्त करना होगा और आमतौर पर सीपीयू के भीतर आंतरिक डेटा पथ या डेटा बस की तुलना में सिस्टम बस में कुछ भी धीमा होता है।

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

यह डेटा के साथ ऐसा करना ठीक है जो स्टैक पर होता है क्योंकि वे अक्सर कैश होते हैं, लेकिन ऐसे डेटा के लिए नहीं जिन्हें इसके मेमोरी एड्रेस को हर पुनरावृत्ति के लिए क्वेरी करना पड़ता है।

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

आपके पास एक ही एल्गोरिथ्म हो सकता है जो एक ही डेटा सेट से संबंधित है, लेकिन आप इसके स्टैक वेरिएंट के लिए एक कार्यान्वयन डिजाइन और दूसरा उसके ढेर-आवंटित संस्करण के लिए बस इसलिए चाहते हैं क्योंकि उपरोक्त समस्या जो O(n)एल्गोरिथ्म की अपनी जटिलता से देखी जाती है जब काम कर रही हो ढेर के साथ।

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

यदि आप सही अनुकूलन चाहते हैं, हाँ यह कोड दोहराव जैसा लग सकता है, लेकिन इसे सामान्य करने के लिए एक ही एल्गोरिथ्म के दो वेरिएंट होना अधिक कुशल होगा। स्टैक ऑपरेशन्स के लिए एक, और हीप ऑपरेशन्स के लिए दूसरा जो पुनरावृत्त छोरों में किया जाता है!

यहाँ एक छद्म उदाहरण है: दो सरल संरचनाएं, एक एल्गोरिथ्म।

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{} 
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}                

template<typename T>
void Foo( T& t ) {
    // do something with t
}

// some looping operation: first stack then heap.

// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops... 

for (int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.

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


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

@PeterMortensen मैंने अपने मूल उत्तर को थोड़ा संशोधित करके आपकी सलाह को ध्यान में रखा है। मेरा मानना ​​है कि यह वही है जो आप सुझाव दे रहे थे।
फ्रांसिस कुगलर

2

यह पुराना C ++ और अनुकूलन हो सकता है। अपने कंप्यूटर पर मैंने लगभग समान गति प्राप्त की:

एक लूप: 1.577 एमएस

दो छोरों: 1.507 एमएस

मैं एक E5-1620 3.5 GHz प्रोसेसर पर 16 जीबी रैम के साथ विजुअल स्टूडियो 2015 चलाता हूं।

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