दरअसल, C ++ 11 के बाद से, नकल की लागत std::vector
ज्यादातर मामलों में चली गई है।
हालांकि, किसी को यह ध्यान रखना चाहिए कि नए वेक्टर के निर्माण की लागत (फिर उसे नष्ट करना) अभी भी मौजूद है, और मूल्य द्वारा लौटने के बजाय आउटपुट मापदंडों का उपयोग करना तब भी उपयोगी है जब आप वेक्टर की क्षमता का पुन: उपयोग करना चाहते हैं। यह C ++ कोर दिशानिर्देशों के F.20 में एक अपवाद के रूप में प्रलेखित है।
आइए तुलना करें:
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
साथ में:
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
अब, मान लें कि हमें इन विधियों numIter
को एक तंग लूप में कॉल करने की आवश्यकता है , और कुछ कार्रवाई करें। उदाहरण के लिए, आइए सभी तत्वों के योग की गणना करें।
का उपयोग करते हुए BuildLargeVector1
, आप करेंगे:
size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
का उपयोग करते हुए BuildLargeVector2
, आप करेंगे:
size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
पहले उदाहरण में, कई अनावश्यक गतिशील आवंटन / डीलोकेशन हो रहे हैं, जो दूसरे उदाहरण में पुराने तरीके से आउटपुट पैरामीटर का उपयोग करके रोका जाता है, पहले से आवंटित स्मृति का पुन: उपयोग कर रहा है। यह अनुकूलन करने योग्य है या नहीं, यह कंप्यूटिंग / मूल्यों को परिवर्तित करने की लागत की तुलना में आवंटन / सौदे की लागत पर निर्भर करता है।
बेंचमार्क
के मूल्यों के साथ खेलते हैं vecSize
और numIter
। हम वसीयत को * संख्यात्मक रूप से स्थिर रखेंगे ताकि "सिद्धांत रूप में", इसे उसी समय (= असाइनमेंट और परिवर्धन की समान संख्या, सटीक समान मानों) के साथ लिया जाए, और समय का अंतर केवल लागत से आ सकता है आवंटन, सौदे, और कैश का बेहतर उपयोग।
अधिक विशेष रूप से, चलो vecSize का उपयोग करें * numIter = 2 ^ 31 = 2147483648, क्योंकि मेरे पास 16GB RAM है और यह संख्या सुनिश्चित करती है कि 8GB से अधिक आवंटित नहीं किया गया है (sizeof (int) = 4), यह सुनिश्चित करना कि मैं डिस्क पर स्वैप नहीं कर रहा हूं ( अन्य सभी कार्यक्रम बंद कर दिए गए थे, परीक्षण चलाने के दौरान मेरे पास ~ 15GB उपलब्ध था)।
यहाँ कोड है:
#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>
class Timer {
using clock = std::chrono::steady_clock;
using seconds = std::chrono::duration<double>;
clock::time_point t_;
public:
void tic() { t_ = clock::now(); }
double toc() const { return seconds(clock::now() - t_).count(); }
};
std::vector<int> BuildLargeVector1(size_t vecSize) {
return std::vector<int>(vecSize, 1);
}
void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
v.assign(vecSize, 1);
}
int main() {
Timer t;
size_t vecSize = size_t(1) << 31;
size_t numIter = 1;
std::cout << std::setw(10) << "vecSize" << ", "
<< std::setw(10) << "numIter" << ", "
<< std::setw(10) << "time1" << ", "
<< std::setw(10) << "time2" << ", "
<< std::setw(10) << "sum1" << ", "
<< std::setw(10) << "sum2" << "\n";
while (vecSize > 0) {
t.tic();
size_t sum1 = 0;
{
for (int i = 0; i < numIter; ++i) {
std::vector<int> v = BuildLargeVector1(vecSize);
sum1 = std::accumulate(v.begin(), v.end(), sum1);
}
}
double time1 = t.toc();
t.tic();
size_t sum2 = 0;
{
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
BuildLargeVector2(/*out*/ v, vecSize);
sum2 = std::accumulate(v.begin(), v.end(), sum2);
}
} // deallocate v
double time2 = t.toc();
std::cout << std::setw(10) << vecSize << ", "
<< std::setw(10) << numIter << ", "
<< std::setw(10) << std::fixed << time1 << ", "
<< std::setw(10) << std::fixed << time2 << ", "
<< std::setw(10) << sum1 << ", "
<< std::setw(10) << sum2 << "\n";
vecSize /= 2;
numIter *= 2;
}
return 0;
}
और यहाँ परिणाम है:
$ g++ -std=c++11 -O3 main.cpp && ./a.out
vecSize, numIter, time1, time2, sum1, sum2
2147483648, 1, 2.360384, 2.356355, 2147483648, 2147483648
1073741824, 2, 2.365807, 1.732609, 2147483648, 2147483648
536870912, 4, 2.373231, 1.420104, 2147483648, 2147483648
268435456, 8, 2.383480, 1.261789, 2147483648, 2147483648
134217728, 16, 2.395904, 1.179340, 2147483648, 2147483648
67108864, 32, 2.408513, 1.131662, 2147483648, 2147483648
33554432, 64, 2.416114, 1.097719, 2147483648, 2147483648
16777216, 128, 2.431061, 1.060238, 2147483648, 2147483648
8388608, 256, 2.448200, 0.998743, 2147483648, 2147483648
4194304, 512, 0.884540, 0.875196, 2147483648, 2147483648
2097152, 1024, 0.712911, 0.716124, 2147483648, 2147483648
1048576, 2048, 0.552157, 0.603028, 2147483648, 2147483648
524288, 4096, 0.549749, 0.602881, 2147483648, 2147483648
262144, 8192, 0.547767, 0.604248, 2147483648, 2147483648
131072, 16384, 0.537548, 0.603802, 2147483648, 2147483648
65536, 32768, 0.524037, 0.600768, 2147483648, 2147483648
32768, 65536, 0.526727, 0.598521, 2147483648, 2147483648
16384, 131072, 0.515227, 0.599254, 2147483648, 2147483648
8192, 262144, 0.540541, 0.600642, 2147483648, 2147483648
4096, 524288, 0.495638, 0.603396, 2147483648, 2147483648
2048, 1048576, 0.512905, 0.609594, 2147483648, 2147483648
1024, 2097152, 0.548257, 0.622393, 2147483648, 2147483648
512, 4194304, 0.616906, 0.647442, 2147483648, 2147483648
256, 8388608, 0.571628, 0.629563, 2147483648, 2147483648
128, 16777216, 0.846666, 0.657051, 2147483648, 2147483648
64, 33554432, 0.853286, 0.724897, 2147483648, 2147483648
32, 67108864, 1.232520, 0.851337, 2147483648, 2147483648
16, 134217728, 1.982755, 1.079628, 2147483648, 2147483648
8, 268435456, 3.483588, 1.673199, 2147483648, 2147483648
4, 536870912, 5.724022, 2.150334, 2147483648, 2147483648
2, 1073741824, 10.285453, 3.583777, 2147483648, 2147483648
1, 2147483648, 20.552860, 6.214054, 2147483648, 2147483648

(इंटेल i7-7700K @ 4.20GHz; 16GB DDR4 2400Mhz; कुबंटु 18.04)
संकेतन: मेम (v) = v.size () * sizeof (int) = v.size () * 4 मेरे मंच पर।
आश्चर्य नहीं, जब numIter = 1
(यानी, मेम (v) = 8 जीबी), समय पूरी तरह से समान हैं। दरअसल, दोनों ही मामलों में हम केवल 8GB मेमोरी के एक विशाल वेक्टर को एक बार आवंटित कर रहे हैं। यह भी साबित करता है कि BuildLargeVector1 () का उपयोग करते समय कोई प्रतिलिपि नहीं हुई: मेरे पास प्रतिलिपि करने के लिए पर्याप्त RAM नहीं होगी!
जब numIter = 2
, दूसरी वेक्टर को फिर से आवंटित करने के बजाय वेक्टर क्षमता का पुन: उपयोग करना 1.37x तेज है।
जब numIter = 256
, वेक्टर क्षमता का पुन: उपयोग करना (एक वेक्टर को फिर से 256 बार से अधिक बार आवंटित / निपटाने के बजाय ...) 2.xxx2x :)
हम देख सकते हैं कि time1 से बहुत अधिक स्थिर numIter = 1
है numIter = 256
, जिसका अर्थ है कि 8GB के एक विशाल वेक्टर को आवंटित करना 32MB के 256 वैक्टर को आवंटित करने के रूप में महंगा है। हालांकि, 8 जीबी के एक विशाल वेक्टर को आवंटित करना 32 एमबी के एक वेक्टर को आवंटित करने की तुलना में निश्चित रूप से अधिक महंगा है, इसलिए वेक्टर की क्षमता का पुन: उपयोग प्रदर्शन लाभ प्रदान करता है।
से numIter = 512
(मेम (v) = 16MB) से numIter = 8M
(मेम (v) = 1kB) मधुर स्थान है: दोनों विधियां बिल्कुल समान हैं, और संख्यावाचक और vecSize के अन्य सभी संयोजनों की तुलना में तेज हैं। यह शायद इस तथ्य के साथ करना है कि मेरे प्रोसेसर का एल 3 कैश आकार 8 एमबी है, जिससे वेक्टर बहुत अधिक कैश में पूरी तरह से फिट बैठता है। मैं वास्तव में यह नहीं समझाता कि time1
मेम (v) = 16MB के लिए अचानक क्यों कूद रहा है, यह सिर्फ मेम (v) = 8MB के बाद होने के लिए अधिक तार्किक प्रतीत होगा। ध्यान दें कि आश्चर्यजनक रूप से, इस प्यारी जगह में, पुन: उपयोग करने की क्षमता वास्तव में थोड़ी तेज नहीं है! मैं वास्तव में यह नहीं समझाता हूँ।
जब numIter > 8M
चीजें बदसूरत होने लगती हैं। दोनों विधियाँ धीमी हो जाती हैं लेकिन सदिश को मान से वापस करना और भी धीमा हो जाता है। सबसे खराब स्थिति में, केवल एक एकल वाले वेक्टर के साथ int
, मूल्य द्वारा वापस जाने के बजाय पुन: उपयोग करने की क्षमता 3.3x तेज है। संभवतः, यह मालोक () की निर्धारित लागतों के कारण है जो हावी होने लगते हैं।
ध्यान दें कि समय 2 के लिए वक्र समय 1 के लिए वक्र की तुलना में कैसे चिकना है: न केवल वेक्टर क्षमता को फिर से उपयोग करना आम तौर पर तेज है, लेकिन शायद अधिक महत्वपूर्ण बात, यह अधिक पूर्वानुमान है ।
यह भी ध्यान दें कि मीठे स्थान में, हम ~ 0.5s में 64 बिट पूर्णांक के 2 बिलियन अतिरिक्त प्रदर्शन करने में सक्षम थे, जो कि 4.2Ghz 64 बिट प्रोसेसर पर काफी इष्टतम है। हम सभी 8 कोर (एक समय में केवल एक कोर का उपयोग करता है, जिसे मैंने सीपीयू उपयोग की निगरानी करते समय परीक्षण को फिर से चलाकर सत्यापित किया है) का उपयोग करने के लिए अभिकलन को समानांतर करके बेहतर कर सकता है। सबसे अच्छा प्रदर्शन तब प्राप्त किया जाता है जब मेम (v) = 16kB, जो L1 कैश की परिमाण का क्रम है (i7-7700K के लिए L1 डेटा कैश 4x32kB है)।
बेशक, अंतर कम और कम प्रासंगिक हो जाते हैं और अधिक कम्प्यूटेशन जो आपको वास्तव में डेटा पर करना है। नीचे परिणामों अगर हम बदलने के हैं sum = std::accumulate(v.begin(), v.end(), sum);
द्वारा for (int k : v) sum += std::sqrt(2.0*k);
:

निष्कर्ष
- मूल्य द्वारा लौटने के बजाय आउटपुट मापदंडों का उपयोग करना क्षमता का पुन: उपयोग करके प्रदर्शन लाभ प्रदान कर सकता है।
- आधुनिक डेस्कटॉप कंप्यूटर पर, यह केवल बड़े वैक्टर (> 16 एमबी) और छोटे वैक्टर (<1kB) पर लागू होता है।
- छोटे वैक्टर (<1kB) के लाखों / अरबों के आवंटन से बचें। यदि संभव हो, तो क्षमता का पुनः उपयोग करें, या बेहतर अभी तक, अपनी वास्तुकला को अलग तरह से डिजाइन करें।
परिणाम अन्य प्लेटफार्मों पर भिन्न हो सकते हैं। हमेशा की तरह, यदि प्रदर्शन मायने रखता है, तो अपने विशिष्ट उपयोग मामले के लिए बेंचमार्क लिखें।