ใน 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คือ rvalue ดังนั้น v จะถูกสร้างโดยใช้ตัวสร้างการย้ายstd::vectorโดยสมมติว่า (N) RVO ไม่เกิดขึ้น

แม้กระทั่งก่อน C ++ 0x รูปแบบแรกมักจะ "มีประสิทธิภาพ" เนื่องจาก (N) RVO อย่างไรก็ตาม (N) RVO ขึ้นอยู่กับดุลยพินิจของคอมไพเลอร์ ตอนนี้เรามีการอ้างอิง rvalue แล้วจึงรับประกันได้ว่าจะไม่มีสำเนาลึกเกิดขึ้น

แก้ไข : คำถามไม่เกี่ยวกับการเพิ่มประสิทธิภาพ ทั้งสองรูปแบบที่แสดงมีประสิทธิภาพใกล้เคียงกันในโปรแกรมจริง ในขณะที่ในอดีตรูปแบบแรกอาจมีประสิทธิภาพที่แย่ลงตามลำดับความสำคัญ ด้วยเหตุนี้รูปแบบแรกจึงเป็นกลิ่นรหัสที่สำคัญในการเขียนโปรแกรม C ++ มาเป็นเวลานาน ไม่อีกแล้วฉันหวังว่า?


18
ใครเคยบอกว่ามันเป็นรูปแบบที่ไม่ดีในการเริ่มต้น?
Edward Strange

7
แน่นอนว่ามันเป็นกลิ่นรหัสที่ไม่ดีใน "สมัยก่อน" ซึ่งเป็นที่ที่ฉันมาจาก :-)
เนท

1
ฉันหวังว่าอย่างนั้น! ฉันต้องการเห็นมูลค่าการส่งต่อได้รับความนิยมมากขึ้น :)
sellibitze

คำตอบ:


73

เดฟอับราฮัมมีการวิเคราะห์ที่ครอบคลุมสวยของความเร็วในการส่งผ่าน / ค่ากลับมา

คำตอบสั้น ๆ หากคุณต้องการส่งคืนค่าให้ส่งคืนค่า อย่าใช้การอ้างอิงเอาต์พุตเพราะคอมไพเลอร์ทำอย่างไรก็ตาม แน่นอนว่ามีข้อควรระวังดังนั้นคุณควรอ่านบทความนั้น


24
"คอมไพเลอร์ก็ทำอยู่ดี": คอมไพเลอร์ไม่จำเป็นต้องทำเช่นนั้น == ความไม่แน่นอน == ความคิดที่ไม่ดี (ต้องการความแน่นอน 100%) "การวิเคราะห์ที่ครอบคลุม" มีปัญหาอย่างมากกับการวิเคราะห์นั้น - ต้องอาศัยคุณลักษณะของภาษาที่ไม่มีเอกสาร / ไม่เป็นมาตรฐานในคอมไพเลอร์ที่ไม่รู้จัก ("แม้ว่ามาตรฐานจะไม่จำเป็นต้องมีการคัดลอกสำเนา") ดังนั้นแม้ว่ามันจะใช้งานได้ แต่ก็ไม่ควรใช้มัน - ไม่มีการรับประกันอย่างแน่นอนว่ามันจะทำงานได้ตามที่ตั้งใจไว้และไม่มีการรับประกันว่าคอมไพเลอร์ทุกตัวจะทำงานในลักษณะนี้เสมอไป การใช้เอกสารนี้เป็นแนวทางปฏิบัติในการเข้ารหัสที่ไม่ดี IMO แม้ว่าคุณจะสูญเสียประสิทธิภาพก็ตาม
SigTerm

5
@SigTerm: นั่นคือความคิดเห็นที่ยอดเยี่ยม !!! บทความอ้างอิงส่วนใหญ่คลุมเครือเกินกว่าที่จะพิจารณาเพื่อใช้ในการผลิต ผู้คนคิดว่าสิ่งใดก็ตามที่ผู้เขียนเขียนหนังสือ Red In-Depth เป็นพระกิตติคุณและควรยึดถือโดยไม่ต้องคิดหรือวิเคราะห์เพิ่มเติม ATM ไม่มีคอมไพเลอร์ในตลาดที่ให้ copy-elison ได้แตกต่างจากตัวอย่างที่ Abrahams ใช้ในบทความ
Hippicoder

13
@SigTerm มีหลายสิ่งที่คอมไพเลอร์ไม่จำเป็นต้องทำ แต่คุณคิดว่ามันทำต่อไป คอมไพเลอร์ไม่ "จำเป็น" ในการเปลี่ยนx / 2เป็นx >> 1for ints แต่คุณคิดว่ามันจะเป็นเช่นนั้น มาตรฐานยังไม่ได้บอกว่าไม่มีอะไรเกี่ยวกับวิธีที่คอมไพเลอร์จำเป็นต้องใช้ในการอ้างอิง แต่คุณคิดว่าพวกเขาได้รับการจัดการอย่างมีประสิทธิภาพโดยใช้พอยน์เตอร์ มาตรฐานยังไม่ได้บอกอะไรเกี่ยวกับตาราง v ดังนั้นคุณจึงไม่แน่ใจว่าการเรียกฟังก์ชันเสมือนนั้นมีประสิทธิภาพเช่นกัน โดยพื้นฐานแล้วคุณต้องศรัทธาในคอมไพเลอร์ในบางครั้ง
Peter Alexander

16
@Sig: มีการรับประกันน้อยมากยกเว้นผลลัพธ์จริงของโปรแกรมของคุณ หากคุณต้องการความมั่นใจ 100% เกี่ยวกับสิ่งที่กำลังจะเกิดขึ้น 100% คุณควรเปลี่ยนไปใช้ภาษาอื่นทันที
Dennis Zickefoose

6
@SigTerm: ฉันทำงานกับ "สถานการณ์จริงกรณี" ฉันทดสอบสิ่งที่คอมไพเลอร์ทำและทำงานกับสิ่งนั้น ไม่มี "อาจทำงานช้าลง" มันไม่ทำงานช้าลงเพราะคอมไพเลอร์ใช้ RVO ไม่ว่ามาตรฐานจะต้องการหรือไม่ก็ตาม ไม่มี ifs, buts หรือ maybes มันเป็นเพียงข้อเท็จจริงง่ายๆ
Peter Alexander

37

อย่างน้อย IMO มักเป็นความคิดที่ไม่ดี แต่ไม่ใช่ด้วยเหตุผลด้านประสิทธิภาพ เป็นความคิดที่ไม่ดีเพราะโดยปกติแล้วฟังก์ชันที่เป็นปัญหาควรเขียนเป็นอัลกอริทึมทั่วไปที่สร้างเอาต์พุตผ่านตัววนซ้ำ เกือบทุกรหัสที่ยอมรับหรือส่งคืนคอนเทนเนอร์แทนที่จะดำเนินการกับตัวทำซ้ำควรถูกพิจารณาว่าเป็นผู้ต้องสงสัย

อย่าเข้าใจฉันผิด: มีหลายครั้งที่ควรส่งผ่านวัตถุที่คล้ายคอลเลกชัน (เช่นสตริง) แต่สำหรับตัวอย่างที่อ้างถึงฉันจะพิจารณาว่าการส่งหรือคืนเวกเตอร์เป็นความคิดที่ไม่ดี


6
ปัญหาเกี่ยวกับวิธีการวนซ้ำคือคุณต้องสร้างฟังก์ชันและเมธอดให้เป็นเทมเพลตแม้ว่าจะรู้จักประเภทองค์ประกอบคอลเลกชันก็ตาม นี่เป็นสิ่งที่น่ารำคาญและเมื่อวิธีการที่เป็นปัญหาเสมือนจริงเป็นไปไม่ได้ โปรดทราบว่าฉันไม่เห็นด้วยกับคำตอบของคุณต่อ แต่ในทางปฏิบัติมันก็ค่อนข้างยุ่งยากใน C ++
jon-hanson

22
ฉันต้องไม่เห็นด้วย การใช้ตัววนซ้ำสำหรับเอาต์พุตในบางครั้งก็เหมาะสม แต่ถ้าคุณไม่ได้เขียนอัลกอริทึมทั่วไปโซลูชันทั่วไปมักจะให้ค่าใช้จ่ายที่หลีกเลี่ยงไม่ได้ซึ่งยากที่จะพิสูจน์ได้ ทั้งในแง่ของความซับซ้อนของโค้ดและประสิทธิภาพที่แท้จริง
Dennis Zickefoose

1
@ เดนนิส: ฉันต้องบอกว่าประสบการณ์ของฉันค่อนข้างตรงกันข้าม: ฉันเขียนหลาย ๆ อย่างเป็นเทมเพลตแม้ว่าฉันจะรู้ประเภทที่เกี่ยวข้องล่วงหน้าก็ตามเพราะการทำเช่นนั้นง่ายกว่าและช่วยเพิ่มประสิทธิภาพ
Jerry Coffin

9
ฉันเองคืนตู้คอนเทนเนอร์ เจตนาชัดเจนรหัสง่ายกว่าฉันไม่สนใจประสิทธิภาพมากนักเมื่อเขียน (ฉันแค่หลีกเลี่ยงการมองโลกในแง่ร้าย แต่เนิ่นๆ) ฉันไม่แน่ใจว่าการใช้ตัววนซ้ำเอาท์พุตจะทำให้เจตนาของฉันชัดเจนขึ้นหรือไม่ ... และฉันต้องการโค้ดที่ไม่ใช่เทมเพลตให้มากที่สุดเพราะในการอ้างอิงโปรเจ็กต์ขนาดใหญ่จะฆ่าการพัฒนา
Matthieu M.

1
@ เดนนิส: ฉันจะวางแนวความคิดนั้นคุณไม่ควร"สร้างคอนเทนเนอร์แทนที่จะเขียนลงในช่วง" คอนเทนเนอร์ก็แค่นั้น - คอนเทนเนอร์ ข้อกังวลของคุณ (และข้อกังวลเกี่ยวกับรหัสของคุณ) ควรอยู่ที่เนื้อหาไม่ใช่ที่เก็บ
Jerry Coffin

18

สาระสำคัญคือ:

Copy Elision และ RVO สามารถหลีกเลี่ยง "สำเนาที่น่ากลัว" ได้ (คอมไพเลอร์ไม่จำเป็นต้องใช้การเพิ่มประสิทธิภาพเหล่านี้และในบางสถานการณ์ก็ไม่สามารถใช้งานได้)

การอ้างอิง RValue C ++ 0x อนุญาตให้ใช้สตริง / เวกเตอร์ที่รับประกันได้

หากคุณสามารถละทิ้งการใช้งานคอมไพเลอร์ / STL รุ่นเก่าได้ให้คืนเวกเตอร์อย่างอิสระ (และตรวจสอบให้แน่ใจว่าออบเจ็กต์ของคุณรองรับด้วย) หากฐานรหัสของคุณต้องรองรับคอมไพเลอร์ "น้อยกว่า" ให้ยึดติดกับรูปแบบเดิม ๆ

น่าเสียดายที่มีอิทธิพลสำคัญต่ออินเทอร์เฟซของคุณ ถ้า C ++ 0x ไม่ใช่ตัวเลือกและคุณต้องการการค้ำประกันคุณอาจใช้อ็อบเจ็กต์ที่นับอ้างอิงหรือคัดลอกเมื่อเขียนแทนในบางสถานการณ์ แม้ว่าพวกเขาจะมีข้อเสียเกี่ยวกับมัลติเธรด

(ฉันหวังว่าคำตอบเดียวใน C ++ จะง่ายและตรงไปตรงมาและไม่มีเงื่อนไข)


11

อันที่จริงตั้งแต่ C ++ 11 ค่าใช้จ่ายของการคัดลอกstd::vectorจะหายไปในกรณีส่วนใหญ่

แต่หนึ่งควรเก็บไว้ในใจว่าค่าใช้จ่ายของการสร้างเวกเตอร์ใหม่ (แล้วdestructingมัน) ยังคงมีอยู่และการใช้พารามิเตอร์ที่ส่งออกแทนที่จะกลับโดยค่ายังคงมีประโยชน์เมื่อคุณต้องการที่จะนำมาใช้กำลังการผลิตเวกเตอร์ของ เอกสารนี้ถือเป็นข้อยกเว้นในF.20ของ C ++ Core Guidelines

เปรียบเทียบกัน:

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);
}

ในตัวอย่างแรกมีการจัดสรร / การจัดสรรแบบไดนามิกที่ไม่จำเป็นเกิดขึ้นมากมายซึ่งถูกป้องกันในตัวอย่างที่สองโดยใช้พารามิเตอร์เอาต์พุตด้วยวิธีเก่าโดยนำหน่วยความจำที่จัดสรรไปแล้วกลับมาใช้ใหม่ การเพิ่มประสิทธิภาพนี้จะคุ้มค่าหรือไม่นั้นขึ้นอยู่กับต้นทุนสัมพัทธ์ของการจัดสรร / การจัดสรรเทียบกับต้นทุนในการคำนวณ / การเปลี่ยนค่า

เกณฑ์มาตรฐาน

เล่น Let 's มีค่าของและvecSize numIterเราจะคงค่าคงที่ vecSize * numIter ไว้เพื่อให้ "ในทางทฤษฎี" ควรใช้เวลาเท่ากัน (= มีการกำหนดและการเพิ่มจำนวนเท่ากันโดยมีค่าเท่ากันทุกประการ) และความแตกต่างของเวลาอาจมาจากต้นทุนของ การจัดสรรการจัดสรรและการใช้แคชที่ดีขึ้น

โดยเฉพาะอย่างยิ่งให้ใช้ vecSize * numIter = 2 ^ 31 = 2147483648 เพราะฉันมี RAM 16GB และหมายเลขนี้ทำให้มั่นใจได้ว่าไม่เกิน 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

ผลการเปรียบเทียบ

(Intel i7-7700K @ 4.20GHz; 16GB DDR4 2400Mhz; Kubuntu 18.04)

สัญกรณ์: mem (v) = v.size () * sizeof (int) = v.size () * 4 บนแพลตฟอร์มของฉัน

ไม่น่าแปลกใจที่เมื่อnumIter = 1(เช่น mem (v) = 8GB) เวลาจะเหมือนกันอย่างสมบูรณ์แบบ ในทั้งสองกรณีเราจะจัดสรรเวกเตอร์ขนาดใหญ่ 8GB ในหน่วยความจำเพียงครั้งเดียว สิ่งนี้ยังพิสูจน์ได้ว่าไม่มีการคัดลอกเกิดขึ้นเมื่อใช้ BuildLargeVector1 (): ฉันไม่มี RAM เพียงพอที่จะทำสำเนา!

เมื่อnumIter = 2นำความจุเวกเตอร์กลับมาใช้ใหม่แทนที่จะจัดสรรเวกเตอร์ที่สองใหม่จะเร็วขึ้น 1.37 เท่า

เมื่อnumIter = 256ใช้ความจุเวกเตอร์ซ้ำ (แทนที่จะจัดสรร / ยกเลิกการจัดสรรเวกเตอร์ซ้ำแล้วซ้ำอีก 256 ครั้ง ... ) เร็วขึ้น 2.45 เท่า :)

เราสามารถสังเกตได้ว่า time1 นั้นค่อนข้างคงที่จากnumIter = 1ถึงnumIter = 256ซึ่งหมายความว่าการจัดสรรเวกเตอร์ขนาดใหญ่ 8GB หนึ่งตัวนั้นค่อนข้างแพงพอ ๆ กับการจัดสรรเวกเตอร์ 256 เวกเตอร์ 32MB อย่างไรก็ตามการจัดสรรเวกเตอร์ขนาดใหญ่จำนวน 8GB หนึ่งตัวนั้นมีราคาแพงกว่าการจัดสรรเวกเตอร์หนึ่งตัวที่มีขนาด 32MB ดังนั้นการนำความจุของเวกเตอร์กลับมาใช้ใหม่จะช่วยเพิ่มประสิทธิภาพ

จากnumIter = 512(mem (v) = 16MB) ถึงnumIter = 8M(mem (v) = 1kB) คือจุดที่น่าสนใจ: ทั้งสองวิธีนั้นเร็วและเร็วกว่าการรวมกันของ numIter และ vecSize อื่น ๆ ทั้งหมด สิ่งนี้อาจเกี่ยวข้องกับความจริงที่ว่าขนาดแคช L3 ของโปรเซสเซอร์ของฉันคือ 8MB ดังนั้นเวกเตอร์จึงพอดีกับแคชอย่างสมบูรณ์ ฉันไม่ได้อธิบายจริงๆว่าทำไมการกระโดดอย่างกะทันหันtime1คือสำหรับ mem (v) = 16MB ดูเหมือนว่าจะมีเหตุผลมากกว่าที่จะเกิดขึ้นหลังจากนั้นเมื่อ mem (v) = 8MB โปรดทราบว่าในจุดที่น่าสนใจนี้ความสามารถในการไม่ใช้ซ้ำนั้นเร็วกว่าเล็กน้อย! ฉันไม่อธิบายเรื่องนี้จริงๆ

เมื่อnumIter > 8Mสิ่งต่างๆเริ่มน่าเกลียด ทั้งสองวิธีทำงานช้าลง แต่การส่งคืนเวกเตอร์ตามค่าจะช้าลง ในกรณีที่เลวร้ายที่สุดด้วยเวกเตอร์ที่มีเพียงหนึ่งเดียวการintใช้ความจุซ้ำแทนที่จะส่งคืนตามค่าจะเร็วกว่า 3.3 เท่า น่าจะเป็นเพราะต้นทุนคงที่ของ malloc () ซึ่งเริ่มมีอิทธิพลเหนือกว่า

หมายเหตุวิธีโค้งสำหรับ time2 เป็นนุ่มนวลกว่าโค้งสำหรับ time1: ไม่เพียง แต่เรื่องการใช้กำลังการผลิตเวกเตอร์โดยทั่วไปได้เร็วขึ้น แต่อาจสำคัญกว่าก็จะมากขึ้นคาดเดาได้

นอกจากนี้โปรดทราบว่าในจุดที่น่าสนใจเราสามารถเพิ่มจำนวนเต็ม 64 บิตได้ 2 พันล้านใน ~ 0.5s ซึ่งค่อนข้างดีที่สุดในโปรเซสเซอร์ 4.2Ghz 64 บิต เราสามารถทำได้ดีขึ้นโดยการคำนวณแบบขนานเพื่อใช้ทั้ง 8 คอร์ (การทดสอบข้างต้นใช้เพียงครั้งละหนึ่งคอร์ซึ่งฉันได้ตรวจสอบแล้วโดยรันการทดสอบซ้ำในขณะที่ตรวจสอบการใช้งาน CPU) ประสิทธิภาพที่ดีที่สุดจะทำได้เมื่อ mem (v) = 16kB ซึ่งเป็นลำดับของขนาดของแคช L1 (แคชข้อมูล L1 สำหรับ i7-7700K คือ 4x32kB)

แน่นอนว่าความแตกต่างมีความเกี่ยวข้องน้อยลงเรื่อย ๆ เมื่อคุณต้องคำนวณข้อมูลมากขึ้น ด้านล่างนี้คือผลลัพธ์หากเราแทนที่sum = std::accumulate(v.begin(), v.end(), sum);ด้วยfor (int k : v) sum += std::sqrt(2.0*k);:

เกณฑ์มาตรฐาน 2

ข้อสรุป

  1. การใช้พารามิเตอร์เอาต์พุตแทนการส่งคืนตามค่าอาจให้ประสิทธิภาพที่เพิ่มขึ้นจากการใช้ความจุซ้ำ
  2. ในคอมพิวเตอร์เดสก์ท็อปสมัยใหม่ดูเหมือนว่าจะใช้ได้กับเวกเตอร์ขนาดใหญ่ (> 16MB) และเวกเตอร์ขนาดเล็ก (<1kB) เท่านั้น
  3. หลีกเลี่ยงการจัดสรรเวกเตอร์ขนาดเล็กหลายล้าน / พันล้าน (<1kB) ถ้าเป็นไปได้ให้ใช้ความจุซ้ำหรือดีกว่าให้ออกแบบสถาปัตยกรรมของคุณให้แตกต่างออกไป

ผลลัพธ์อาจแตกต่างกันไปในแพลตฟอร์มอื่น ตามปกติหากประสิทธิภาพมีความสำคัญให้เขียนเกณฑ์มาตรฐานสำหรับกรณีการใช้งานเฉพาะของคุณ


6

ฉันยังคงคิดว่ามันเป็นการปฏิบัติที่ไม่ดี แต่ก็น่าสังเกตว่าทีมของฉันใช้ MSVC 2008 และ GCC 4.1 ดังนั้นเราจึงไม่ได้ใช้คอมไพเลอร์ล่าสุด

ก่อนหน้านี้ฮอตสปอตจำนวนมากที่แสดงใน vtune ด้วย MSVC 2008 มาจากการคัดลอกสตริง เรามีรหัสดังนี้:

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

... โปรดทราบว่าเราใช้ประเภท String ของเราเอง (จำเป็นต้องใช้เนื่องจากเรามีชุดพัฒนาซอฟต์แวร์ที่ผู้เขียนปลั๊กอินสามารถใช้คอมไพเลอร์ที่แตกต่างกันดังนั้นการใช้งาน std :: string / std :: wstring ที่แตกต่างกันและไม่เข้ากัน)

ฉันทำการเปลี่ยนแปลงง่ายๆเพื่อตอบสนองต่อเซสชันการสร้างโปรไฟล์การสุ่มตัวอย่างกราฟการโทรที่แสดง String :: String (const String &) ซึ่งต้องใช้เวลาเป็นจำนวนมาก วิธีการดังในตัวอย่างข้างต้นเป็นผู้ให้ข้อมูลที่ดีที่สุด (จริงๆแล้วเซสชันการทำโปรไฟล์แสดงให้เห็นการจัดสรรหน่วยความจำและการจัดสรรตำแหน่งเป็นหนึ่งในฮอตสปอตที่ใหญ่ที่สุดโดยมีตัวสร้างสำเนาสตริงเป็นผู้สนับสนุนหลักสำหรับการจัดสรร)

การเปลี่ยนแปลงที่ฉันทำนั้นง่ายมาก:

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

สิ่งนี้ทำให้โลกแตกต่าง! ฮอตสปอตหายไปในเซสชันผู้สร้างโปรไฟล์ที่ตามมาและนอกจากนี้เรายังทำการทดสอบหน่วยอย่างละเอียดอีกมากมายเพื่อติดตามประสิทธิภาพแอปพลิเคชันของเรา เวลาทดสอบประสิทธิภาพทุกประเภทลดลงอย่างมากหลังจากการเปลี่ยนแปลงง่ายๆเหล่านี้

สรุป: เราไม่ได้ใช้คอมไพเลอร์ล่าสุดแบบสัมบูรณ์ แต่ดูเหมือนว่าเราจะไม่สามารถพึ่งพาคอมไพเลอร์ที่ปรับแต่งการคัดลอกเพื่อส่งคืนตามค่าได้อย่างน่าเชื่อถือ (อย่างน้อยก็ไม่ใช่ในทุกกรณี) นั่นอาจไม่ใช่กรณีสำหรับผู้ที่ใช้คอมไพเลอร์รุ่นใหม่เช่น MSVC 2010 ฉันรอคอยเมื่อเราสามารถใช้ C ++ 0x และเพียงแค่ใช้การอ้างอิงค่า rvalue และไม่ต้องกังวลว่าเราจะมองโค้ดของเราในแง่ร้ายโดยการคืนค่าคอมเพล็กซ์ คลาสตามค่า

[แก้ไข] ตามที่ Nate ชี้ให้เห็น RVO ใช้กับการส่งคืนชั่วคราวที่สร้างขึ้นภายในฟังก์ชัน ในกรณีของฉันไม่มีจังหวะดังกล่าว (ยกเว้นสาขาที่ไม่ถูกต้องซึ่งเราสร้างสตริงว่าง) ดังนั้น RVO จะไม่สามารถใช้งานได้


3
นั่นคือสิ่งนี้: RVO ขึ้นอยู่กับคอมไพเลอร์ แต่คอมไพเลอร์ C ++ 0x ต้องใช้ความหมายการย้ายหากตัดสินใจไม่ใช้ RVO (สมมติว่ามีตัวสร้างการย้าย) การใช้ตัวดำเนินการ Trigraph เอาชนะ RVO ดูcpp-next.com/archive/2009/09/move-it-with-rvalue-referencesที่ Peter อ้างถึง แต่ตัวอย่างของคุณไม่มีสิทธิ์สำหรับการย้ายความหมายเนื่องจากคุณไม่ได้ส่งคืนชั่วคราว
เนท

@ Stinky472: การส่งคืนสมาชิกตามค่ามักจะช้ากว่าการอ้างอิงเสมอ การอ้างอิง Rvalue จะยังคงช้ากว่าการส่งคืนการอ้างอิงไปยังสมาชิกดั้งเดิม (หากผู้โทรสามารถใช้การอ้างอิงแทนการต้องการสำเนา) นอกจากนี้ยังมีหลายครั้งที่คุณสามารถบันทึกการอ้างอิง rvalue ได้เนื่องจากคุณมีบริบท ตัวอย่างเช่นคุณสามารถทำ String newstring; newstring.resize (string1.size () + string2.size () + ... ); newstring + = string1; newstring + = string2; ฯลฯ นี่ยังคงเป็นการประหยัดค่า rvalues ​​ได้มาก
ลูกสุนัข

@DeadMG ประหยัดมากกว่าตัวดำเนินการไบนารี + แม้จะมีคอมไพเลอร์ C ++ 0x ที่ใช้ RVO? ถ้าเป็นเช่นนั้นก็น่าเสียดาย จากนั้นอีกครั้งที่ makse รู้สึกเนื่องจากเรายังคงต้องสร้างชั่วคราวเพื่อคำนวณสตริงที่ต่อกันในขณะที่ + = สามารถเชื่อมต่อโดยตรงกับสตริงใหม่
stinky472

แล้วกรณีเช่น: string newstr = str1 + str2; ในคอมไพเลอร์ที่ใช้ความหมายการย้ายดูเหมือนว่าควรจะเร็วเท่ากับหรือเร็วกว่า: string newstr; newstr + = str1; newstr + = str2; ไม่มีเงินสำรองดังนั้นที่จะพูด (ฉันสมมติว่าคุณหมายถึงการจองแทนที่จะปรับขนาด)
stinky472

5
@Nate: ฉันคิดว่าคุณมีความสับสนtrigraphsเหมือน<::หรือ??!กับผู้ประกอบการที่มีเงื่อนไข ?: (บางครั้งเรียกว่าผู้ประกอบ ternary )
fredoverflow

3

เพียงเพื่อ nitpick เล็กน้อย: มันไม่บ่อยในภาษาโปรแกรมหลายภาษาที่จะส่งคืนอาร์เรย์จากฟังก์ชัน โดยส่วนใหญ่การอ้างอิงถึงอาร์เรย์จะถูกส่งกลับ ใน C ++ การเปรียบเทียบที่ใกล้เคียงที่สุดจะกลับมาboost::shared_array


4
@Billy: std :: vector เป็นประเภทค่าที่มีความหมายสำเนา มาตรฐาน C ++ ปัจจุบันไม่มีการรับประกันว่า (N) RVO จะถูกนำไปใช้และในทางปฏิบัติมีสถานการณ์ในชีวิตจริงมากมายเมื่อไม่ได้เป็นเช่นนั้น
Nemanja Trifunovic

3
@Billy: อีกครั้งมีสถานการณ์จริงบางอย่างที่แม้แต่คอมไพเลอร์ล่าสุดก็ไม่ใช้ NRVO: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic

3
@Billy ONeal: 99% ไม่เพียงพอคุณต้องการ 100% กฎของเมอร์ฟี - "ถ้ามีอะไรผิดพลาดก็จะ" ความไม่แน่นอนเป็นสิ่งที่ดีหากคุณกำลังจัดการกับตรรกะที่คลุมเครือ แต่ก็ไม่ใช่ความคิดที่ดีสำหรับการเขียนซอฟต์แวร์แบบเดิม หากมีความเป็นไปได้เพียง 1% ที่โค้ดไม่ทำงานอย่างที่คุณคิดคุณควรคาดหวังว่าโค้ดนี้จะแนะนำข้อผิดพลาดที่สำคัญซึ่งจะทำให้คุณถูกไล่ออก แถมยังไม่ใช่คุณสมบัติมาตรฐานอีกด้วย การใช้คุณสมบัติที่ไม่มีเอกสารเป็นความคิดที่ไม่ดีหากในหนึ่งปีนับจากทราบว่าคอมไพเลอร์จะทิ้งฟีเจอร์ ( มาตรฐานไม่จำเป็นใช่ไหม) คุณจะเป็นคนที่มีปัญหา
SigTerm

4
@SigTerm: ถ้าเรากำลังพูดถึงความถูกต้องของพฤติกรรมฉันจะเห็นด้วยกับคุณ อย่างไรก็ตามเรากำลังพูดถึงการเพิ่มประสิทธิภาพ สิ่งเหล่านี้ใช้ได้ดีโดยมีความแน่นอนน้อยกว่า 100%
Billy ONeal

2
@Nemanja: ฉันไม่เห็นสิ่งที่ "พึ่งพา" ที่นี่ แอปของคุณทำงานเหมือนเดิมไม่ว่าจะใช้ RVO หรือ NRVO ก็ตาม หากพวกเขาใช้มันจะทำงานได้เร็วขึ้น หากแอปของคุณทำงานช้าเกินไปบนแพลตฟอร์มใดแพลตฟอร์มหนึ่งและคุณตรวจสอบย้อนกลับเพื่อส่งคืนการคัดลอกค่าให้ทำการเปลี่ยนแปลง แต่นั่นไม่ได้เปลี่ยนความจริงที่ว่าแนวทางปฏิบัติที่ดีที่สุดคือยังคงใช้ค่าส่งคืน หากคุณต้องการอย่างแท้จริงเพื่อให้แน่ใจว่าไม่มีการคัดลอกเกิดขึ้นให้ห่อเวกเตอร์shared_ptrเป็นวัน ๆ และเรียกมันว่าวัน
Billy ONeal

2

หากผลการดำเนินงานเป็นเรื่องจริงที่คุณควรตระหนักว่าความหมายย้ายไม่ได้เสมอเร็วกว่าการคัดลอก ตัวอย่างเช่นหากคุณมีสตริงที่ใช้การเพิ่มประสิทธิภาพสตริงขนาดเล็กสำหรับสตริงขนาดเล็กตัวสร้างการย้ายจะต้องทำงานในปริมาณเดียวกันกับตัวสร้างสำเนาทั่วไป


1
NRVO ไม่ได้หายไปเพียงเพราะมีการเพิ่มตัวสร้างการเคลื่อนย้าย
Billy ONeal

1
@ บิลลี่จริง แต่ไม่เกี่ยวข้องคำถามคือ C ++ 0x เปลี่ยนแนวทางปฏิบัติที่ดีที่สุดและ NRVO ไม่ได้เปลี่ยนแปลงเนื่องจาก C ++ 0x
Motti
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.