अनियमित रैंडम पढ़ने से लगता है कि काम करना अच्छा है - क्यों?


18

निम्नलिखित बहुत सरल कंप्यूटर प्रोग्राम पर विचार करें:

for i = 1 to n:
    y[i] = x[p[i]]

यहाँ और वाई हैं n बाइट्स की तत्व सरणियों, और पी एक है n शब्दों का तत्व सरणी। यहाँ n बड़ा है, उदाहरण के लिए, n = 2 31 (ताकि डेटा का केवल एक नगण्य अंश किसी भी प्रकार की कैश मेमोरी में फिट हो)।एक्सynपीnnn=231

मान लें कि में यादृच्छिक संख्याएँ हैं , समान रूप से 1 और n के बीच वितरित की जाती हैं ।पी1n

आधुनिक हार्डवेयर के दृष्टिकोण से, इसका मतलब निम्न होना चाहिए:

  • पढ़ना सस्ता है (अनुक्रमिक रीड)पी[मैं]
  • पढ़ने बहुत महंगा है (यादृच्छिक पढ़ता है; लगभग सभी पढ़ता कैश छूट जाए हैं, हम मुख्य स्मृति से प्रत्येक व्यक्ति बाइट लाने के लिए करना होगा)एक्स[पी[मैं]]
  • लेखन सस्ते (अनुक्रमिक लिखने) है।y[मैं]

और यह वास्तव में मैं क्या देख रहा हूँ। प्रोग्राम की तुलना में प्रोग्राम बहुत धीमा है जो केवल अनुक्रमिक पढ़ता है और लिखता है। महान।

अब सवाल आता है: यह कार्यक्रम आधुनिक मल्टी-कोर प्लेटफार्मों पर कितनी अच्छी तरह से समानांतर है?


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

हालाँकि, यह तब नहीं था जब मैंने कुछ एल्गोरिदम के साथ प्रयोग करना शुरू किया, जहां अड़चन इस तरह का था!

मैंने बस भोले के लिए लूप को एक ओपनएमपी समानांतर-लूप के साथ बदल दिया (संक्षेप में, यह सिर्फ रेंज को छोटे भागों में विभाजित करेगा और इन भागों को समानांतर में विभिन्न सीपीयू कोर पर चलाएगा)।[1,n]

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

  • 2 x 4-कोर Xeon (कुल 8 कोर में): एकल-थ्रेडेड संस्करण की तुलना में कारक 5-8 स्पीडअप।

  • 2 x 6-कोर Xeon (कुल 12 कोर में): एकल-थ्रेडेड संस्करण की तुलना में कारक 8-14 स्पीडअप।

अब यह पूरी तरह से अप्रत्याशित था। प्रशन:

  1. सटीक रूप से इस तरह का कार्यक्रम इतनी अच्छी तरह से समानांतर क्यों करता है ? हार्डवेयर में क्या होता है? (मेरा वर्तमान अनुमान इन पंक्तियों के साथ कुछ है: अलग-अलग धागे से यादृच्छिक रीड "पाइपलाइड" हैं और इन पर उत्तर प्राप्त करने की औसत दर एक एकल धागे के मामले की तुलना में बहुत अधिक है।)

  2. क्या किसी भी स्पीडअप को प्राप्त करने के लिए कई थ्रेड्स और कई कोर का उपयोग करना आवश्यक है ? Pipelining किसी तरह का वास्तव में मुख्य स्मृति और CPU के बीच इंटरफेस में जगह लेता है, तो एक-थ्रेडेड आवेदन मुख्य स्मृति पता है नहीं कर सका है कि यह जल्द ही की आवश्यकता होगी , एक्स [ पी [ मैं + 1 ] ] , ... और कंप्यूटर मुख्य मेमोरी से संबंधित कैश लाइनों को लाना शुरू कर सकता है? यदि यह सिद्धांत रूप में संभव है, तो मैं इसे अभ्यास में कैसे प्राप्त कर सकता हूं?एक्स[पी[मैं]]एक्स[पी[मैं+1]]

  3. सही सैद्धांतिक मॉडल क्या है जिसका उपयोग हम इस तरह के कार्यक्रमों का विश्लेषण करने के लिए कर सकते हैं (और प्रदर्शन की सही भविष्यवाणी करते हैं)?


संपादित करें: अब कुछ स्रोत कोड और बेंचमार्क परिणाम यहां उपलब्ध हैं: https://github.com/suomela/parallel-random-read

बॉलपार्क के कुछ उदाहरण ( ):n=232

  • लगभग। एक धागे के साथ प्रति नेशन (यादृच्छिक पढ़ें) 42 एन एस
  • लगभग। 12 कोर के साथ पुनरावृत्ति (यादृच्छिक पढ़ें) प्रति 5 एन एस।

जवाबों:


9

पीnपीnपीपी

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

nn/पीपी

n=231

n

अंत में, QSM (क्यूइंग शेयर्ड मेमोरी) के अलावा , मुझे किसी अन्य सैद्धांतिक समानांतर मॉडल के बारे में पता नहीं है, उसी स्तर पर साझा मेमोरी (आपके मामले में, ओपनएमपी का उपयोग करते समय) मुख्य मेमोरी कोर के बीच साझा की जाती है। , और कैश हमेशा कोर के बीच साझा किया जाता है)। वैसे भी, भले ही मॉडल दिलचस्प है, लेकिन इसे बड़ी सफलता नहीं मिली।


1
यह देखने में भी मदद कर सकता है क्योंकि प्रत्येक कोर मेमोरी लेवल समानता की अधिक या कम निश्चित मात्रा प्रदान करता है, उदाहरण के लिए, एक निश्चित समय में 10 x [] भार प्रक्रिया में। साझा L3 में हिट के 0.5% संभावना के साथ, एक एकल थ्रेड में मुख्य मेमोरी प्रतिक्रिया की प्रतीक्षा करने के लिए उन सभी भारों की आवश्यकता के लिए 0.995 ** 10 (95 +%) मौका होगा। 6 कोर के साथ कुल 60 x [] लंबित रीड्स प्रदान करते हैं, लगभग 26% संभावना है कि कम से कम एक रीड L3 में हिट होगा। इसके अलावा, अधिक एमएलपी, अधिक मेमोरी कंट्रोलर वास्तविक बैंडविड्थ को बढ़ाने के लिए एक्सेस शेड्यूल कर सकता है।
पॉल ए। क्लेटन

5

मैंने खुद __builtin_prefetch () की कोशिश करने का फैसला किया। मैं इसे यहाँ पोस्ट कर रहा हूँ क्योंकि अन्य लोग अपनी मशीनों पर इसका परीक्षण करना चाहते हैं। परिणाम जोका का वर्णन करते हैं, उसके करीब हैं: चलने के समय में लगभग 20% की कमी जब 20 तत्वों को आगे पीछे करना।

परिणाम:

prefetch =   0, time = 1.58000
prefetch =   1, time = 1.47000
prefetch =   2, time = 1.39000
prefetch =   3, time = 1.34000
prefetch =   4, time = 1.31000
prefetch =   5, time = 1.30000
prefetch =   6, time = 1.27000
prefetch =   7, time = 1.28000
prefetch =   8, time = 1.26000
prefetch =   9, time = 1.27000
prefetch =  10, time = 1.27000
prefetch =  11, time = 1.27000
prefetch =  12, time = 1.30000
prefetch =  13, time = 1.29000
prefetch =  14, time = 1.30000
prefetch =  15, time = 1.28000
prefetch =  16, time = 1.24000
prefetch =  17, time = 1.28000
prefetch =  18, time = 1.29000
prefetch =  19, time = 1.25000
prefetch =  20, time = 1.24000
prefetch =  19, time = 1.26000
prefetch =  18, time = 1.27000
prefetch =  17, time = 1.26000
prefetch =  16, time = 1.27000
prefetch =  15, time = 1.28000
prefetch =  14, time = 1.29000
prefetch =  13, time = 1.26000
prefetch =  12, time = 1.28000
prefetch =  11, time = 1.30000
prefetch =  10, time = 1.31000
prefetch =   9, time = 1.27000
prefetch =   8, time = 1.32000
prefetch =   7, time = 1.31000
prefetch =   6, time = 1.30000
prefetch =   5, time = 1.27000
prefetch =   4, time = 1.33000
prefetch =   3, time = 1.38000
prefetch =   2, time = 1.41000
prefetch =   1, time = 1.41000
prefetch =   0, time = 1.59000

कोड:

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

void cracker(int *y, int *x, int *p, int n, int pf) {
    int i;
    int saved = pf;  /* let compiler optimize address computations */

    for (i = 0; i < n; i++) {
        __builtin_prefetch(&x[p[i+saved]]);
        y[i] += x[p[i]];
    }
}

int main(void) {
    int n = 50000000;
    int *x, *y, *p, i, pf, k;
    clock_t start, stop;
    double elapsed;

    /* set up arrays */
    x = malloc(sizeof(int)*n);
    y = malloc(sizeof(int)*n);
    p = malloc(sizeof(int)*n);
    for (i = 0; i < n; i++)
        p[i] = rand()%n;

    /* warm-up exercise */
    cracker(y, x, p, n, pf);

    k = 20;
    for (pf = 0; pf < k; pf++) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }
    for (pf = k; pf >= 0; pf--) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }

    return 0;
}

4
  1. DDR3 पहुंच वास्तव में पाइपलाइज्ड है। http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf स्लाइड 20 और 24 शो में पता चलता है कि पाइपलाइन किए गए रीड ऑपरेशन के दौरान मेमोरी बस में क्या होता है।

  2. (आंशिक रूप से गलत, नीचे देखें) यदि सीपीयू आर्किटेक्चर कैश प्रीफैच का समर्थन करता है तो कई धागे आवश्यक नहीं हैं। आधुनिक x86 और एआरएम के साथ-साथ कई अन्य आर्किटेक्चर में एक स्पष्ट प्रीफ़ैच निर्देश है। कई अतिरिक्त रूप से मेमोरी एक्सेस में पैटर्न का पता लगाने और स्वचालित रूप से प्रीफ़ेटिंग करने का प्रयास करते हैं। सॉफ़्टवेयर समर्थन कंपाइलर-विशिष्ट है, उदाहरण के लिए GCC और क्लैंग में __builtin_prefech () स्पष्ट प्रीफ़ेटिंग के लिए आंतरिक है।

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

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

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


__builtin_prefech बहुत आशाजनक लगता है। दुर्भाग्य से, मेरे त्वरित प्रयोगों में यह एकल-थ्रेड प्रदर्शन के साथ मदद नहीं करता था (<10%)। इस तरह के आवेदन में मुझे कितने बड़े सुधार की उम्मीद करनी चाहिए?
जुल्का सुमेला

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

मेरा पहला प्रयास एक सरणी में संग्रहीत एक निश्चित यादृच्छिक क्रम बना रहा था, फिर उस क्रम में पूर्वनिर्मित ( gist.github.com/osimola/7917602 ) के साथ और इसके बिना पुनरावृत्ति । एक कोर i5 पर लगभग 2% का अंतर आया। ऐसा लगता है कि या तो प्रीफ़च सभी पर काम नहीं करता है या हार्डवेयर प्रेडिक्टर अप्रत्यक्ष रूप से समझता है।
जुहानी सिमोला

1
तो, उसके लिए परीक्षण, दूसरा प्रयास ( gist.github.com/osimola/7917568 ) एक निश्चित यादृच्छिक बीज द्वारा उत्पन्न अनुक्रम में मेमोरी तक पहुंचता है। इस बार, प्रीफ़ैचिंग संस्करण गैर-प्रीफ़ेचिंग से लगभग 2 गुना तेज था और 1 कदम आगे प्रीफेटिंग की तुलना में 3 गुना तेज था। ध्यान दें कि प्रीफ़ैचिंग संस्करण गैर-प्रीफ़ेटिंग संस्करण की तुलना में मेमोरी एक्सेस के प्रति अधिक संगणना करता है।
जुहानी सिमोला

यह मशीन पर निर्भर लगता है। मैंने नीचे पैट मोरिन के कोड की कोशिश की (उस पोस्ट पर टिप्पणी नहीं कर सकता क्योंकि मेरे पास प्रतिष्ठा नहीं है) और मेरा परिणाम विभिन्न प्रीफ़ैच मूल्यों के लिए 1.3% के भीतर है।
जुहानी सिमोला
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.