2010-06-03 9 views
34

मेरे पास एक ऐसा कार्य है जो memcpy कर रहा है, लेकिन यह चक्रों की एक बड़ी मात्रा ले रहा है। मेमोरी के टुकड़े को स्थानांतरित करने के लिए memcpy का उपयोग करने से कहीं तेज विकल्प/दृष्टिकोण है?memcpy के लिए तेज विकल्प?

+1

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

उत्तर

111

memcpy स्मृति में चारों ओर बाइट कॉपी करने का सबसे तेज़ तरीका होने की संभावना है। यदि आपको कुछ तेज़ी से चाहिए तो के आसपास की चीजों की प्रतिलिपि बनाने का तरीका निकालने का प्रयास करें, उदा। स्वैप पॉइंटर्स केवल डेटा ही नहीं।

+2

+1, हमें हाल ही में एक समस्या थी जब हमारे कुछ कोड SUDDENLY धीमे हो गए और एक निश्चित फ़ाइल को संसाधित करते समय बहुत सारी अतिरिक्त स्मृति का उपभोग किया।फ़ाइल को बंद करने के लिए कुछ विशाल मेटाडाटा ब्लॉक था जबकि अन्य मक्खियों में मेटाडेटा या छोटे ब्लॉक नहीं थे। और उन मेटाडेटा की प्रतिलिपि बनाई गई, प्रतिलिपि बनाई गई, प्रतिलिपि बनाई गई, दोनों समय और स्मृति का उपभोग किया गया। पास-बाय-कॉन्स्ट-रेफरेंस के साथ प्रतिस्थापित प्रतिलिपि। – sharptooth

+6

यह तेजी से memcpy के बारे में एक अच्छा सवाल है, लेकिन यह जवाब एक कामकाज प्रदान करता है, जवाब नहीं। जैसे http://software.intel.com/en-us/articles/memcpy-performance/ कुछ गंभीर कारण बताते हैं कि क्यों memcpy अक्सर यह कम से कम कुशल हो सकता है। –

+0

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

6

आमतौर पर संकलक के साथ भेजे गए मानक पुस्तकालय memcpy() को पहले से ही लक्षित प्लेटफ़ॉर्म के लिए सबसे तेज़ तरीका लागू करेगा।

3

यह आमतौर पर एक प्रतिलिपि बनाने के लिए तेज़ नहीं है। चाहे आप कॉपी करने के लिए अपने फ़ंक्शन को अनुकूलित कर सकें, मुझे नहीं पता लेकिन यह देखने लायक है।

3

कभी कभी memcpy, memset की तरह काम करता है, ... दो अलग अलग तरीकों से लागू कर रहे हैं:

    एक बार एक असली समारोह
  • एक बार कुछ विधानसभा कि तुरंत inlined है

नहीं सभी के रूप में के रूप में

  • कंपाइलर्स डिफ़ॉल्ट रूप से इनलाइन-असेंबली संस्करण लेते हैं, आपका कंपाइलर डिफ़ॉल्ट रूप से फ़ंक्शन संस्करण का उपयोग कर सकता है, जिससे फ़ंक्शन कॉल के कारण कुछ ओवरहेड होता है। फ़ंक्शन के आंतरिक संस्करण को कैसे लेना है (कमांड लाइन विकल्प, प्रज्ञा, ...) को देखने के लिए अपने कंपाइलर को जांचें।

    संपादित करें: माइक्रोसॉफ्ट सी कंपाइलर पर इंट्रिनिक्स के स्पष्टीकरण के लिए http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx देखें।

  • 0

    मुझे लगता है कि आपके पास मेमोरी का प्रदर्शन आपके लिए कोई मुद्दा बन गया है, तो मुझे लगता है कि आपके पास स्मृति की विशाल क्षेत्र होनी चाहिए?

    इस मामले में, मैं ओपन स्कूल के सुझाव से सहमत था किसी तरह सामान कॉपी करने के लिए नहीं यह पता लगाने की ..

    इसके बजाय स्मृति में से एक विशाल ब्लॉब होने के

    चारों ओर कॉपी करने के लिए जब भी आप उसे बदलना चाहते हैं, तो आप शायद इसके बजाय कुछ वैकल्पिक डेटा संरचनाओं को आजमाएं।

    वास्तव में आपके समस्या क्षेत्र के बारे में कुछ भी जानने के बिना, मैं persistent data structures पर एक अच्छा नज़र डालने का सुझाव दूंगा और या तो अपने आप को लागू कर रहा हूं या मौजूदा कार्यान्वयन का पुन: उपयोग कर रहा हूं।

    2

    आपको कंपाइलर/प्लेटफ़ॉर्म मैनुअल की जांच करें। Memcpy का उपयोग कर कुछ माइक्रो प्रोसेसर और डीएसपी-किट के लिए intrinsic functions या DMA संचालन से बहुत धीमी है।

    2

    यदि आपका प्लेटफ़ॉर्म इसका समर्थन करता है, तो देखें कि क्या आप फ़ाइल में अपना डेटा छोड़ने के लिए mmap() सिस्टम कॉल का उपयोग कर सकते हैं ... आम तौर पर ओएस बेहतर प्रबंधन कर सकता है। और, जैसा कि हर कोई कह रहा है, अगर संभव हो तो कॉपी करने से बचें; पॉइंटर्स इस तरह के मामलों में आपके दोस्त हैं।

    10

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

    कृपया अधिक जानकारी प्रदान करें - आर्किटेक्चर, ऑपरेटिंग सिस्टम (यदि प्रासंगिक हो)।

    +1

    एआरएम के लिए libc impl अब तेज है कि आप स्वयं को बनाने में सक्षम होंगे। छोटी प्रतियों के लिए (पृष्ठ के बाद कुछ भी कम) यह आपके कार्यों के अंदर एएसएम पाश का उपयोग करने के लिए तेज़ हो सकता है। लेकिन, बड़ी प्रतियों के लिए आप libc impl को हरा नहीं पाएंगे, क्योंकि diff प्रोसेसर के पास थोड़ा अधिक "सबसे इष्टतम" कोड पथ हैं। उदाहरण के लिए एक कॉर्टेक्स 8 एनईओएन कॉपी निर्देशों के साथ सबसे अच्छा काम करता है, लेकिन कॉर्टेक्स 9 एलडीएम/एसएमएम एआरएम निर्देशों के साथ तेज है। आप कोड का एक टुकड़ा नहीं लिख सकते जो कि दोनों प्रोसेसर के लिए तेज़ है, लेकिन आप बड़े बफर के लिए केवल memcpy को कॉल कर सकते हैं। – MoDJ

    +0

    @MoDJ: मेरी इच्छा है कि मानक सी लाइब्रेरी में कुछ अलग-अलग memcpy वेरिएंट शामिल हों, जिन मामलों में सभी समान परिभाषित व्यवहार, लेकिन विभिन्न अनुकूलित मामलों और - कुछ में - गठबंधन-बनाम गठबंधन उपयोग के लिए प्रतिबंध। यदि कोड को आम तौर पर छोटी संख्या में बाइट्स या ज्ञात-टू-गठबंधन शब्दों की प्रतिलिपि बनाने की आवश्यकता होती है, तो एक मूर्ख चरित्र-पर-समय के कार्यान्वयन कुछ फैनसीयर memcpy() कार्यान्वयन के लिए कम समय में नौकरी कर सकता है कार्यवाही का क्रम। – supercat

    1

    आप इस पर एक नजर है कर सकते हैं:

    http://www.danielvik.com/2010/02/fast-memcpy-in-c.html

    एक और विचार मैं कोशिश करेगा स्मृति ब्लॉक नकल करने गाय तकनीकों का उपयोग करें और ओएस जैसे ही मांग पर नकल संभाल जाने के लिए है पृष्ठ को लिखा गया है। mmap() का उपयोग कर यहां कुछ संकेत दिए गए हैं: Can I do a copy-on-write memcpy in Linux?

    0

    नंबर सही है, आप इसे बहुत अधिक कॉल कर रहे हैं।

    यह देखने के लिए कि आप इसे कहां से बुला रहे हैं और क्यों, इसे डीबगर के नीचे कुछ बार रोकें और ढेर को देखें।

    0

    स्मृति आमतौर पर CPU के आदेश सेट में समर्थित है, और memcpy आम तौर पर उस का प्रयोग करेंगे। और यह आमतौर पर सबसे तेज़ तरीका है।

    आपको यह जांचना चाहिए कि आपका सीपीयू वास्तव में क्या कर रहा है। लिनक्स पर, स्वैपी इन और आउट और वर्चुअल मेमोरी प्रभावशीलता के साथ सर-बी 1 या वीएमस्टैट 1 या/proc/memstat में देखकर देखें। आप देख सकते हैं कि आपकी प्रतिलिपि को खाली स्थान पर बहुत से पृष्ठों को धक्का देना है, या उन्हें पढ़ना है,

    इसका मतलब यह होगा कि आपकी समस्या प्रतिलिपि के लिए उपयोग में नहीं है, लेकिन आपका सिस्टम कैसे उपयोग करता है याद। आपको फ़ाइल कैश को कम करने या पहले लिखना शुरू करना पड़ सकता है, या पृष्ठों को स्मृति में लॉक करना पड़ सकता है।

    6

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

    Very fast memcpy for image processing?

    यदि आप खोज करते हैं, तो आप अन्य कार्यान्वयन भी पा सकते हैं। लेकिन सच गति के लिए आपको एक असेंबली संस्करण की आवश्यकता है।

    +0

    मैंने एसएसई 2 का उपयोग करके इस तरह के कोड को आजमाया। यह मेरे एएमडी सिस्टम पर बिल्टिन की तुलना में 4x के कारक से धीमा था। अगर आप इसकी मदद कर सकते हैं तो कॉपी करना हमेशा बेहतर होता है। – Matt

    +0

    हालांकि 'memmove' को ओवरलैप के लिए जांचना और संभालना होगा, ऐसा करने के लिए' memcpy' की आवश्यकता नहीं है। बड़ी समस्या यह है कि बड़े ब्लॉक की प्रतिलिपि करते समय कुशल होने के लिए, 'memcpy' के कार्यान्वयन को काम शुरू करने से पहले एक प्रतिलिपि दृष्टिकोण का चयन करने की आवश्यकता होती है। अगर कोड को बाइट्स की मनमानी संख्या की प्रतिलिपि बनाने में सक्षम होना चाहिए, लेकिन वह संख्या उस समय का 9 0%, समय का दो 9%, समय का तीन 0.9% इत्यादि और 'गिनती 'के मूल्य, 'dest', और' src' की आवश्यकता नहीं होगी, फिर एक इनलाइन वाली 'अगर (गिनती) करें * dest + = * src; जबकि (- गिनती> 0); '" स्मार्ट "दिनचर्या से बेहतर हो सकता है। – supercat

    +0

    बीटीडब्ल्यू, कुछ एम्बेडेड सिस्टम पर, एक अन्य कारण 'memcpy' सबसे तेज़ दृष्टिकोण नहीं हो सकता है कि एक डीएमए नियंत्रक कभी-कभी सीपीयू की तुलना में कम ओवरहेड के साथ स्मृति के ब्लॉक की प्रतिलिपि बनाने में सक्षम हो सकता है, लेकिन प्रतिलिपि करने का सबसे प्रभावी तरीका डीएमए शुरू करने के लिए हो सकता है और फिर डीएमए चल रहा है, जबकि अन्य प्रसंस्करण कर सकते हैं। अलग-अलग फ्रंट-एंड कोड और डेटा बसों वाली प्रणाली पर, डीएमए को कॉन्फ़िगर करना संभव हो सकता है ताकि यह प्रत्येक चक्र पर डेटा कॉपी करे जब CPU को किसी अन्य चीज़ के लिए डेटा बस की आवश्यकता न हो। यह प्रतिलिपि के लिए सीपीयू का उपयोग करने से बेहतर प्रदर्शन प्राप्त कर सकता है ... – supercat

    1

    आपको अपने कोड के लिए जेनरेट किया गया असेंबली कोड देखना चाहिए। जो आप नहीं चाहते हैं वह memcpy कॉल मानक लाइब्रेरी में memcpy फ़ंक्शन पर कॉल उत्पन्न करता है - जो आप चाहते हैं कि सबसे बड़ी डेटा की प्रतिलिपि बनाने के लिए सर्वोत्तम एएसएम निर्देश को बार-बार कॉल करना है - rep movsq जैसे कुछ।

    आप इसे कैसे प्राप्त कर सकते हैं? खैर, संकलक memcpy पर इसेएस के साथ बदलकर कॉल को अनुकूलित करता है जब तक कि यह जानता है कि इसे कितना डेटा कॉपी करना चाहिए। यदि आप एक अच्छी तरह से निर्धारित (constexpr) मान के साथ memcpy लिखते हैं तो आप इसे देख सकते हैं। यदि संकलक मूल्य को नहीं जानता है, तो उसे memcpy के बाइट-स्तरीय कार्यान्वयन पर वापस आना होगा - यह समस्या यह है कि memcpy को एक बाइट ग्रैन्युलरिटी का सम्मान करना होगा। यह अभी भी 128 बिट्स को एक समय में ले जायेगा, लेकिन प्रत्येक 128 बी के बाद इसे जांचना होगा कि इसमें 128 बी के रूप में प्रतिलिपि बनाने के लिए पर्याप्त डेटा है या इसे 64 बिट्स पर वापस गिरना है, फिर 32 और 8 (मुझे लगता है कि 16 उप-उपनिवेशीय हो सकता है) वैसे भी, लेकिन मुझे निश्चित रूप से पता नहीं है)।

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

    6

    यह AV862 निर्देश सेट के साथ x86_64 का उत्तर है। हालांकि सिम के साथ एआरएम/एएआरएच 64 के लिए कुछ ऐसा ही लागू हो सकता है।

    रेजन 1800 एक्स पर एकल मेमोरी चैनल पूरी तरह से भरा हुआ है (प्रत्येक में 2 स्लॉट, 16 जीबी डीडीआर 4), निम्नलिखित कोड एमएसवीसी ++ 2017 कंपाइलर पर memcpy() से 1.56 गुना तेज है। यदि आप 2 डीडीआर 4 मॉड्यूल के साथ दोनों मेमोरी चैनल भरते हैं, यानी आपके पास सभी 4 डीडीआर 4 स्लॉट व्यस्त हैं, तो आपको 2 गुना तेज स्मृति प्रतिलिपि मिल सकती है। ट्रिपल- (क्वाड-) चैनल मेमोरी सिस्टम के लिए, यदि कोड को समान AVX512 कोड तक बढ़ाया गया है तो आप 1.5 (2.0) बार तेज मेमोरी कॉपी कर सकते हैं। व्यस्त सभी स्लॉट वाले AVX2-only ट्रिपल/क्वाड चैनल सिस्टम के साथ तेज़ी से लोड होने की उम्मीद नहीं है क्योंकि उन्हें पूरी तरह से लोड करने के लिए आपको 32 बाइट्स से अधिक लोड/स्टोर करने की आवश्यकता है (ट्रिपल के लिए 48 बाइट- और क्वाड-चैनल के लिए 64-बाइट्स सिस्टम), जबकि AVX2 एक बार में 32 बाइट से अधिक लोड/स्टोर नहीं कर सकता है। हालांकि कुछ सिस्टम पर मल्टीथ्रेडिंग AVX512 या यहां तक ​​कि AVX2 के बिना इसे कम कर सकती है।

    तो यहां कॉपी कोड है जो मानता है कि आप स्मृति के एक बड़े ब्लॉक की प्रतिलिपि बना रहे हैं जिसका आकार 32 का एक बहु है और ब्लॉक 32-बाइट गठबंधन है।

    गैर-एकाधिक आकार और गैर-गठबंधन ब्लॉक के लिए, प्रस्तावना/epilogue कोड चौड़ाई को 16 (एसएसई 4.1), 8, 4, 2 और आखिरकार 1 बाइट ब्लॉक ब्लॉक और पूंछ के लिए कम किया जा सकता है । इसके अलावा मध्य में 2-3 __m256i मानों की एक स्थानीय सरणी स्रोत से गठबंधन पढ़ने और गंतव्य के लिए गठबंधन लिखने के बीच प्रॉक्सी के रूप में उपयोग की जा सकती है। जब सीपीयू कैश शामिल है (_stream_ बिना अर्थात AVX निर्देश उपयोग किया जाता है), प्रति गति कई बार अपने सिस्टम पर चला जाता है:

    #include <immintrin.h> 
    #include <cstdint> 
    /* ... */ 
    void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) { 
        assert(nBytes % 32 == 0); 
        assert((intptr_t(pvDest) & 31) == 0); 
        assert((intptr_t(pvSrc) & 31) == 0); 
        const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc); 
        __m256i *pDest = reinterpret_cast<__m256i*>(pvDest); 
        int64_t nVects = nBytes/sizeof(*pSrc); 
        for (; nVects > 0; nVects--, pSrc++, pDest++) { 
        const __m256i loaded = _mm256_stream_load_si256(pSrc); 
        _mm256_stream_si256(pDest, loaded); 
        } 
        _mm_sfence(); 
    } 
    

    इस कोड की एक प्रमुख विशेषता यह है कि यह सीपीयू कैश को छोड़ देता है जब नकल है।

    मेरी डीडीआर 4 मेमोरी 2.6GHz CL13 है।

    memcpy(): 17 208 004 271 bytes/sec. 
    Stream copy: 26 842 874 528 bytes/sec. 
    

    ध्यान दें कि इन मापों में दोनों इनपुट और आउटपुट बफ़र्स का कुल आकार बीत सेकंड की संख्या से विभाजित किया गया है: तो जब एक सरणी से डेटा के 8GB को कॉपी मैं निम्नलिखित गति मिल गया। क्योंकि सरणी के प्रत्येक बाइट के लिए 2 मेमोरी एक्सेस हैं: इनपुट इनपुट से बाइट पढ़ने के लिए, दूसरा आउटपुट सरणी में बाइट लिखने के लिए। दूसरे शब्दों में, जब एक सरणी से दूसरे में 8 जीबी की प्रतिलिपि बनाते हैं, तो आप 16 जीबी मेमोरी एक्सेस ऑपरेशंस करते हैं।

    मॉडरेट मल्टीथ्रेडिंग 1.44 गुना के प्रदर्शन में और सुधार कर सकती है, इसलिए memcpy() से अधिक की वृद्धि मेरी मशीन पर 2.55 गुना तक पहुंच जाती है। यहाँ कैसे धारा प्रतिलिपि प्रदर्शन मेरी मशीन पर इस्तेमाल धागे की संख्या पर निर्भर करता है:

    Stream copy 1 threads: 27114820909.821 bytes/sec 
    Stream copy 2 threads: 37093291383.193 bytes/sec 
    Stream copy 3 threads: 39133652655.437 bytes/sec 
    Stream copy 4 threads: 39087442742.603 bytes/sec 
    Stream copy 5 threads: 39184708231.360 bytes/sec 
    Stream copy 6 threads: 38294071248.022 bytes/sec 
    Stream copy 7 threads: 38015877356.925 bytes/sec 
    Stream copy 8 threads: 38049387471.070 bytes/sec 
    Stream copy 9 threads: 38044753158.979 bytes/sec 
    Stream copy 10 threads: 37261031309.915 bytes/sec 
    Stream copy 11 threads: 35868511432.914 bytes/sec 
    Stream copy 12 threads: 36124795895.452 bytes/sec 
    Stream copy 13 threads: 36321153287.851 bytes/sec 
    Stream copy 14 threads: 36211294266.431 bytes/sec 
    Stream copy 15 threads: 35032645421.251 bytes/sec 
    Stream copy 16 threads: 33590712593.876 bytes/sec 
    

    कोड है:

    void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) { 
        for (; nVects > 0; nVects--, pSrc++, pDest++) { 
        const __m256i loaded = _mm256_stream_load_si256(pSrc); 
        _mm256_stream_si256(pDest, loaded); 
        } 
    } 
    
    void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) { 
        assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0); 
        const uint32_t maxThreads = std::thread::hardware_concurrency(); 
        std::vector<std::thread> thrs; 
        thrs.reserve(maxThreads + 1); 
    
        const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput); 
        __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput); 
        const int64_t nVects = cnDoubles * sizeof(*gpdInput)/sizeof(*pSrc); 
    
        for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) { 
        auto start = std::chrono::high_resolution_clock::now(); 
        lldiv_t perWorker = div((long long)nVects, (long long)nThreads); 
        int64_t nextStart = 0; 
        for (uint32_t i = 0; i < nThreads; i++) { 
         const int64_t curStart = nextStart; 
         nextStart += perWorker.quot; 
         if ((long long)i < perWorker.rem) { 
         nextStart++; 
         } 
         thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart); 
        } 
        for (uint32_t i = 0; i < nThreads; i++) { 
         thrs[i].join(); 
        } 
        _mm_sfence(); 
        auto elapsed = std::chrono::high_resolution_clock::now() - start; 
        double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count(); 
        printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double)/nSec); 
    
        thrs.clear(); 
        } 
    }