C ++ में, किसी फ़ंक्शन से वेक्टर को वापस करने के लिए अभी भी बुरा अभ्यास है?


103

लघु संस्करण: बड़ी वस्तुओं को लौटाना आम बात है - जैसे कि कई प्रोग्रामिंग भाषाओं में वैक्टर / सरणियाँ-। क्या यह शैली अब C ++ 0x में स्वीकार्य है यदि कक्षा में एक मूव कंस्ट्रक्टर है, या C ++ प्रोग्रामर इसे अजीब / बदसूरत / घृणा मानते हैं?

लंबे संस्करण: C ++ 0x में यह अभी भी बुरा रूप माना जाता है?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

पारंपरिक संस्करण इस तरह दिखेगा:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

नए संस्करण में, जिस मान से लौटा गया BuildLargeVectorहै, वह व्याप्त है, इसलिए v का निर्माण कंस्ट्रक्टर के उपयोग से किया जाएगा std::vector, यह मानते हुए (N) RVO जगह नहीं लेता है।

C ++ 0x से पहले भी पहला फॉर्म अक्सर (N) RVO के कारण "कुशल" होगा। हालांकि, (एन) आरवीओ संकलक के विवेक पर है। अब जब हमारे पास संदर्भ संदर्भ हैं तो इसकी गारंटी है कि कोई गहरी प्रति नहीं लगेगी।

संपादित करें : प्रश्न वास्तव में अनुकूलन के बारे में नहीं है। दिखाए गए दोनों रूपों में वास्तविक दुनिया के कार्यक्रमों में लगभग समान प्रदर्शन है। जबकि, अतीत में, पहले रूप में खराब प्रदर्शन का आदेश हो सकता था। परिणामस्वरूप पहला रूप लंबे समय तक C ++ प्रोग्रामिंग में एक प्रमुख कोड गंध था। अब और नहीं, मुझे उम्मीद है?


18
किसने कभी कहा था कि इसकी शुरुआत किस रूप में हुई थी?
एडवर्ड स्ट्रेंज

7
यह निश्चित रूप से "पुराने दिनों" में एक बुरा कोड गंध था, जो कि मैं कहाँ से हूँ। :-)
नैट

1
मुझे यकीन है आशा है! मैं पास-दर-मूल्य को और अधिक लोकप्रिय होते देखना चाहता हूँ। :)
सेलिबिट्ज़

जवाबों:


73

डेव अब्रह्म के पास / वापसी मूल्यों को पारित करने की गति का एक बहुत व्यापक विश्लेषण है ।

संक्षिप्त उत्तर, यदि आपको मान वापस करने की आवश्यकता है तो एक मान लौटाएँ। आउटपुट संदर्भ का उपयोग न करें क्योंकि कंपाइलर इसे वैसे भी करता है। बेशक, कैवियट हैं, इसलिए आपको उस लेख को पढ़ना चाहिए।


24
"कंपाइलर इसे वैसे भी करता है": कंपाइलर को == अनिश्चितता == बुरा विचार (100% निश्चितता की आवश्यकता) करने की आवश्यकता नहीं है। "व्यापक विश्लेषण" उस विश्लेषण के साथ एक बहुत बड़ी समस्या है - यह अज्ञात संकलक में अनिर्दिष्ट / गैर-मानक भाषा सुविधाओं पर निर्भर करता है ("हालांकि मानक द्वारा कॉपी एलिजन की आवश्यकता कभी नहीं होती है")। इसलिए भले ही यह काम करता है, इसका उपयोग करना एक अच्छा विचार नहीं है - बिल्कुल कोई वारंटी नहीं है कि यह उद्देश्य के रूप में काम करेगा, और कोई वारंटी नहीं है कि हर कंपाइलर हमेशा इस तरह से काम करेगा। इस दस्तावेज़ पर भरोसा करना एक खराब कोडिंग अभ्यास है, IMO। भले ही आप प्रदर्शन खो देंगे।
सिग्मर

5
@SigTerm: यह एक उत्कृष्ट टिप्पणी है !!! संदर्भित लेख के अधिकांश उत्पादन में उपयोग के लिए विचार करने के लिए बहुत अस्पष्ट है। लोग कुछ भी सोचते हैं कि एक लेखक जो एक रेड इन-डेप्थ किताब लिखता है वह सुसमाचार है और बिना किसी और विचार या विश्लेषण के इसका पालन किया जाना चाहिए। एटीएम वहाँ बाजार पर एक संकलक नहीं है जो कॉपी-एलिसन को उतना ही विविध प्रदान करता है जितना कि अब्राहम लेख में उदाहरणों का उपयोग करता है।
हिप्पिकोडर

13
@SigTerm, ऐसा बहुत कुछ है जिसे संकलक को करने की आवश्यकता नहीं है, लेकिन आप यह मानते हैं कि यह वैसे भी करता है। कंपाइलर्स को s के लिए बदलने x / 2के x >> 1लिए "आवश्यक" नहीं है int, लेकिन आप यह मान लेंगे। मानक यह भी कहते हैं कि संदर्भों को लागू करने के लिए कंपाइलरों की आवश्यकता कैसे होती है, लेकिन आप यह मानते हैं कि उन्हें पॉइंटर्स का उपयोग करके कुशलता से नियंत्रित किया जाता है। मानक भी v- तालिकाओं के बारे में कुछ नहीं कहता है, इसलिए आप यह सुनिश्चित नहीं कर सकते कि आभासी फ़ंक्शन कॉल कुशल हैं। अनिवार्य रूप से, आपको कई बार कंपाइलर पर विश्वास करने की आवश्यकता होती है।
पीटर अलेक्जेंडर

16
@ सिग: आपके प्रोग्राम के वास्तविक आउटपुट को छोड़कर वास्तव में बहुत कम गारंटी है। यदि आप 100% निश्चितता चाहते हैं कि 100% समय क्या होने जा रहा है, तो आप एक अलग भाषा के लिए एकमुश्त स्विच करना बेहतर समझते हैं।
डेनिस ज़िकेफोज़

6
@SigTerm: मैं "वास्तविक-केस परिदृश्य" पर काम करता हूं। मैं परीक्षण करता हूं कि संकलक क्या करता है और इसके साथ काम करता है। कोई भी "धीमी काम नहीं कर सकता है"। यह बस धीमी गति से काम नहीं करता है क्योंकि संकलक आरवीओ को लागू करता है, मानक को इसकी आवश्यकता है या नहीं। वहाँ कोई ifs, buts, या maybes हैं, यह सिर्फ साधारण तथ्य है।
पीटर एलेक्जेंडर

37

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

मुझे गलत मत समझिए: कई बार संग्रह-जैसी वस्तुओं (जैसे, तार) के आस-पास से गुजरने में समझदारी होती है, लेकिन उदाहरण के लिए उद्धृत, मैं वेक्टर के खराब विचार को पारित या वापस करने पर विचार करूंगा।


6
इटरेटर दृष्टिकोण के साथ समस्या यह है कि आपको फ़ंक्शन और विधियों को अस्थायी रूप से बनाने की आवश्यकता होती है, भले ही संग्रह तत्व प्रकार ज्ञात हो। यह परेशान है, और जब प्रश्न में विधि आभासी है, असंभव है। ध्यान दें, मैं आपके उत्तर के प्रति असहमत नहीं हूं, लेकिन व्यवहार में यह C ++ में थोड़ा बोझिल हो जाता है।
जॉन-हॉनसन

22
मुझे असहमत होना पड़ेगा। आउटपुट के लिए पुनरावृत्तियों का उपयोग करना कभी-कभी उचित होता है, लेकिन यदि आप एक जेनेरिक एल्गोरिथ्म नहीं लिख रहे हैं, तो जेनेरिक समाधान अक्सर अपरिहार्य ओवरहेड प्रदान करते हैं जो कि औचित्य सिद्ध करना कठिन है। दोनों कोड जटिलता और वास्तविक प्रदर्शन के संदर्भ में।
डेनिस ज़िकेफोज़

1
@ डेनिस: मुझे कहना है कि मेरा अनुभव काफी विपरीत रहा है: मैं बहुत सी चीजों को खाके के रूप में लिखता हूं, जब मैं समय से पहले शामिल प्रकारों को जानता हूं, क्योंकि ऐसा करना सरल है और प्रदर्शन में सुधार करता है।
जेरी कॉफिन

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

1
@ डेनिस: मैं उस अवधारणा को सकारात्मक रूप से प्रस्तुत करूंगा, आपको कभी भी "एक सीमा तक लिखने के बजाय एक कंटेनर का निर्माण नहीं करना चाहिए ।" एक कंटेनर सिर्फ इतना है - एक कंटेनर। आपकी चिंता (और आपके कोड की चिंता) सामग्री के साथ होनी चाहिए, कंटेनर से नहीं।
जेरी कॉफिन

18

जिस्ट है:

कॉपी एलिसन और आरवीओ "डरावनी प्रतियां" से बच सकते हैं (इन अनुकूलन को लागू करने के लिए संकलक की आवश्यकता नहीं है, और कुछ स्थितियों में इसे लागू नहीं किया जा सकता है)

C ++ 0x RValue संदर्भ एक स्ट्रिंग / वेक्टर कार्यान्वयन की अनुमति देता है जो इसकी गारंटी देता है।

यदि आप पुराने संकलक / एसटीएल कार्यान्वयन को छोड़ सकते हैं, तो स्वतंत्र रूप से वैक्टर लौटाएं (और सुनिश्चित करें कि आपकी स्वयं की वस्तुएं भी इसका समर्थन करती हैं)। यदि आपके कोड आधार को "कम" संकलक का समर्थन करने की आवश्यकता है, तो पुरानी शैली से चिपके रहें।

दुर्भाग्य से, आपके इंटरफेस पर इसका बड़ा प्रभाव है। यदि C ++ 0x एक विकल्प नहीं है, और आपको गारंटी की आवश्यकता है, तो आप कुछ परिदृश्यों में संदर्भ-गणना या कॉपी-ऑन-राइट ऑब्जेक्ट के बजाय उपयोग कर सकते हैं। वे हालांकि, मल्टीथ्रेडिंग के साथ डाउनसाइड करते हैं।

(मैं चाहता हूं कि C ++ में सिर्फ एक उत्तर सरल और सीधा और बिना किसी शर्त के होगा)।


11

दरअसल, 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);:

बेंचमार्क 2

निष्कर्ष

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

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


6

मुझे अभी भी लगता है कि यह एक बुरा अभ्यास है लेकिन यह ध्यान देने योग्य है कि मेरी टीम MSVC 2008 और GCC 4.1 का उपयोग करती है, इसलिए हम नवीनतम संकलक का उपयोग नहीं कर रहे हैं।

पहले MSVC 2008 के साथ vtune में दिखाए गए बहुत सारे हॉटस्पॉट स्ट्रिंग कॉपी करने के लिए नीचे आए थे। हमारे पास इस तरह का कोड था:

String Something::id() const
{
    return valid() ? m_id: "";
}

... ध्यान दें कि हमने अपने स्वयं के स्ट्रिंग प्रकार का उपयोग किया (यह आवश्यक था क्योंकि हम एक सॉफ्टवेयर डेवलपमेंट किट प्रदान कर रहे हैं जहां प्लगइन लेखक अलग-अलग संकलक का उपयोग कर सकते हैं और इसलिए अलग-अलग, एसटीडी के असंगत कार्यान्वयन :: string / std :: wstring)।

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

मैंने जो परिवर्तन किया वह सरल था:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

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

निष्कर्ष: हम पूर्ण नवीनतम संकलक का उपयोग नहीं कर रहे हैं, लेकिन हम अभी भी संकलक पर निर्भर होने की आशंका को दूर करने के लिए नकल का अनुकूलन नहीं कर सकते हैं, मज़बूती से (सभी मामलों में कम से कम नहीं)। MSVC 2010 जैसे नए संकलक का उपयोग करने वालों के लिए यह मामला नहीं हो सकता है। मैं आगे देख रहा हूं कि हम C ++ 0x का उपयोग कैसे कर सकते हैं और केवल संदर्भ संदर्भों का उपयोग करें और कभी भी चिंता न करें कि हम अपने कोड को जटिल करके वापस ला रहे हैं। मूल्य द्वारा कक्षाएं।

[संपादित करें] जैसा कि नैट ने बताया है, आरवीओ किसी फ़ंक्शन के अंदर बनाई गई लौटेंसीरी पर लागू होता है। मेरे मामले में, ऐसी कोई अस्थायी (अवैध शाखा को छोड़कर, जहां हम एक खाली स्ट्रिंग का निर्माण करते हैं) नहीं थे और इस तरह आरवीओ लागू नहीं होगा।


3
यह बात है: आरवीओ कंपाइलर-डिपेंडेंट है, लेकिन सी ++ 0x कंपाइलर को मूवमेंट शब्द का उपयोग करना चाहिए यदि वह आरवीओ का उपयोग न करने का निर्णय लेता है (यह मानते हुए कि वहाँ एक मूव कंस्ट्रक्टर है)। ट्रिग्राफ ऑपरेटर का उपयोग आरवीओ को हरा देता है। Cpp-next.com/archive/2009/09/move-it-with-rvalue-references देखें जिन्हें पीटर ने संदर्भित किया था। लेकिन आपका उदाहरण वैसे भी कदम शब्दार्थ के लिए योग्य नहीं है क्योंकि आप अस्थायी नहीं लौटे हैं।
नाटे

@ Stinky472: किसी सदस्य को मूल्य देकर लौटना हमेशा संदर्भ की तुलना में धीमा होने वाला था। मूल सदस्य (यदि कॉलर को कॉपी की आवश्यकता के बजाय संदर्भ ले सकता है) का संदर्भ वापस करने की तुलना में रिवेल्यू संदर्भ अभी भी धीमा होगा। इसके अलावा, अभी भी कई बार है कि आप बचा सकते हैं, अधिक संदर्भ संदर्भ में, क्योंकि आपके पास संदर्भ है। उदाहरण के लिए, आप स्ट्रिंग न्यूट्रिंग कर सकते हैं; newstring.resize (string1.size () + string2.size () + ...); newstring + = string1; newstring + = string2; आदि। यह अभी भी प्रतिद्वंद्वियों पर काफी बचत है।
पिल्ला

@DeadMG बाइनरी ऑपरेटर + से भी ज्यादा बचत C ++ 0x कम्पाइलर RVO को लागू करने में करता है? यदि हां, तो यह शर्म की बात है। तब फिर से वह गलत अर्थ है क्योंकि हम अभी भी समाप्‍त स्ट्रिंग की गणना करने के लिए एक अस्थायी बनाने के लिए अंत करते हैं, जबकि + = सीधे newstring में सम्‍मिलित कर सकते हैं।
stinky472

कैसे के बारे में एक मामले की तरह: स्ट्रिंग newstr = str1 + str2; एक कंपाइलर चालित शब्दार्थ को लागू करने पर, ऐसा लगता है कि स्ट्रिंग के रूप में तेजी से या उससे भी तेज होना चाहिए: स्ट्रिंग न्यूट्रस्ट; newstr + = str1; newstr + = str2; कोई रिज़र्व नहीं है, इसलिए बोलने के लिए (मैं मान रहा हूँ कि आप रिज़र्व करने के बजाय रिज़र्व हैं)।
stinky472

5
@Nate: मुझे लगता है कि आप भ्रमित कर रहे हैं trigraphs की तरह <::या ??!के साथ सशर्त ऑपरेटर ?: (कभी कभी कहा जाता है त्रिगुट ऑपरेटर )।
fredoverflow

3

बस थोड़ा सा नाइटपिक करने के लिए: फ़ंक्शन से सरणियों को वापस करने के लिए कई प्रोग्रामिंग भाषाओं में यह आम नहीं है। उनमें से अधिकांश में, सरणी का एक संदर्भ दिया गया है। C ++ में, निकटतम सादृश्य वापस आ जाएगाboost::shared_array


4
@ बिली: std :: वेक्टर एक प्रकार का प्रकार है जिसमें कॉपी शब्दार्थ होता है। वर्तमान सी ++ मानक कोई गारंटी नहीं देता है कि (एन) आरवीओ कभी भी लागू हो जाता है, और व्यवहार में कई वास्तविक जीवन परिदृश्य होते हैं जब यह नहीं होता है।
नवेंजा ट्रिफ़ुनोविक

3
: @Billy: फिर से, वहाँ कुछ बहुत ही वास्तविक परिदृश्यों जहां भी नवीनतम compilers NRVO लागू नहीं है कर रहे हैं efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic

3
@ बिली ओनली: 99% पर्याप्त नहीं है, आपको 100% की आवश्यकता है। मर्फी का नियम - "अगर कुछ गलत हो सकता है, तो यह होगा"। यदि आप किसी तरह के फ़ज़ी लॉजिक से निपट रहे हैं तो अनिश्चितता ठीक है, लेकिन पारंपरिक सॉफ़्टवेयर लिखने के लिए यह एक अच्छा विचार नहीं है। यदि इस संभावना का 1% भी है कि कोड आपके सोचने के तरीके पर काम नहीं करता है, तो आपको उम्मीद करनी चाहिए कि यह कोड महत्वपूर्ण बग को पेश करेगा जो आपको निकाल दिया जाएगा। साथ ही यह एक स्टैंडर्ड फीचर नहीं है। अनिर्धारित सुविधाओं का उपयोग करना एक बुरा विचार है - यदि एक वर्ष में पता से संकलक सुविधा छोड़ देगा (यह मानक, सही द्वारा आवश्यक नहीं है ?), तो आप मुसीबत में होंगे।
सिग्मेट

4
@SigTerm: अगर हम व्यवहार की शुद्धता के बारे में बात कर रहे थे, तो मैं आपसे सहमत होगा। हालांकि, हम एक प्रदर्शन अनुकूलन के बारे में बात कर रहे हैं। ऐसी चीजें 100% से कम निश्चितता के साथ ठीक हैं।
बिली ओनली

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

2

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


1
NRVO दूर नहीं जाता है क्योंकि मूव कंस्ट्रक्टर जोड़े गए थे।
बिली ओनली

1
@ बिली, सत्य लेकिन अप्रासंगिक, सवाल यह है कि C ++ 0x ने सर्वोत्तम प्रथाओं को बदल दिया है और NRVO C ++ 0x के कारण नहीं बदला है
Motti
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.