วิธีที่มีประสิทธิภาพในการคืนค่า std :: vector ใน c ++


110

จำนวนข้อมูลที่ถูกคัดลอกเมื่อส่งคืน std :: vector ในฟังก์ชันและการเพิ่มประสิทธิภาพนั้นจะใหญ่เพียงใดในการวาง std :: vector ใน free-store (บน heap) และส่งกลับตัวชี้แทนนั่นคือ:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

มีประสิทธิภาพมากกว่า:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 

เหรอ?


5
วิธีการส่งเวกเตอร์โดยการอ้างอิงแล้วเติมเข้าไปข้างในf?
Kiril Kirov

4
RVOเป็นการเพิ่มประสิทธิภาพขั้นพื้นฐานที่ค่อนข้างดีที่คอมไพเลอร์ส่วนใหญ่สามารถทำได้
Remus Rusanu

เมื่อมีคำตอบเข้ามาอาจช่วยให้คุณชี้แจงได้ว่าคุณกำลังใช้ C ++ 03 หรือ C ++ 11 แนวทางปฏิบัติที่ดีที่สุดระหว่างสองเวอร์ชันนั้นแตกต่างกันไปเล็กน้อย
Drew Dormann


@ Kiril Kirov ฉันสามารถทำได้โดยไม่ต้องใส่ไว้ในรายการอาร์กิวเมนต์ของฟังก์ชันเช่น เป็นโมฆะ f (std :: vector & result)?
Morten

คำตอบ:


152

ใน C ++ 11 เป็นวิธีที่แนะนำ:

std::vector<X> f();

นั่นคือส่งคืนตามมูลค่า

ด้วย C ++ 11 std::vectorมีความหมายการเคลื่อนที่ซึ่งหมายความว่าเวกเตอร์โลคัลที่ประกาศในฟังก์ชันของคุณจะถูกย้ายเมื่อกลับมาและในบางกรณีคอมไพเลอร์ก็สามารถหลีกเลี่ยงการย้ายได้


2
มันจะถูกย้ายแม้ไม่มีstd::move?
Leonid Volnitsky

14
@LeonidVolnitsky: ใช่ถ้ามันเป็นท้องถิ่น ในความเป็นจริงจะปิดการย้ายตัดออกแม้มันเป็นไปได้ที่มีเพียงreturn std::move(v); return v;ดังนั้นจึงเป็นที่ต้องการ
Nawaz

1
@juanchopanza: ฉันไม่คิดอย่างนั้น ก่อน C ++ 11 คุณสามารถโต้แย้งได้เนื่องจากเวกเตอร์จะไม่ถูกย้าย และ RVO เป็นสิ่งที่ขึ้นอยู่กับคอมไพเลอร์! พูดคุยเกี่ยวกับสิ่งต่างๆในยุค 80 และ 90
Nawaz

2
ความเข้าใจของฉันเกี่ยวกับค่าส่งคืน (ตามค่า) คือ: แทนที่จะเป็น 'ถูกย้าย' ค่าส่งคืนใน callee จะถูกสร้างขึ้นบนสแต็กของผู้โทรดังนั้นการดำเนินการทั้งหมดใน callee จึงอยู่ในสถานที่ไม่มีอะไรต้องย้ายใน RVO . ถูกต้องหรือไม่
r0ng

2
@ r0ng: ใช่นั่นคือความจริง นั่นคือวิธีที่คอมไพเลอร์มักใช้ RVO
Nawaz

74

คุณควรส่งคืนตามมูลค่า

มาตรฐานมีคุณลักษณะเฉพาะในการปรับปรุงประสิทธิภาพของการส่งคืนตามมูลค่า เรียกว่า "copy elision" และโดยเฉพาะอย่างยิ่งในกรณีนี้คือ "named return value optimization (NRVO)"

คอมไพเลอร์ไม่จำเป็นต้องใช้มัน แต่จากนั้นคอมไพเลอร์อีกครั้งก็ไม่จำเป็นต้องใช้ฟังก์ชัน inlining (หรือทำการเพิ่มประสิทธิภาพใด ๆ เลย) แต่ประสิทธิภาพของไลบรารีมาตรฐานอาจค่อนข้างแย่หากคอมไพเลอร์ไม่ปรับให้เหมาะสมและคอมไพเลอร์ที่จริงจังทั้งหมดใช้อินไลน์และ NRVO (และการเพิ่มประสิทธิภาพอื่น ๆ )

เมื่อใช้ NRVO จะไม่มีการคัดลอกในรหัสต่อไปนี้:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

แต่ผู้ใช้อาจต้องการทำสิ่งนี้:

std::vector<int> myvec;
... some time later ...
myvec = f();

การคัดลอกการคัดลอกไม่ได้ป้องกันการคัดลอกที่นี่เนื่องจากเป็นการมอบหมายงานแทนที่จะเป็นการเริ่มต้น อย่างไรก็ตามคุณควรยังคงกลับมาด้วยค่า ใน C ++ 11 งานจะได้รับการปรับให้เหมาะสมโดยสิ่งที่แตกต่างกันเรียกว่า "move semantics" ใน C ++ 03 โค้ดข้างต้นทำให้เกิดการคัดลอกและแม้ว่าในทางทฤษฎีเครื่องมือเพิ่มประสิทธิภาพอาจหลีกเลี่ยงได้ แต่ในทางปฏิบัติมันยากเกินไป ดังนั้นmyvec = f()ใน C ++ 03 คุณควรเขียนสิ่งนี้:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

มีอีกทางเลือกหนึ่งคือการนำเสนออินเทอร์เฟซที่ยืดหยุ่นมากขึ้นให้กับผู้ใช้:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

จากนั้นคุณยังสามารถรองรับอินเทอร์เฟซแบบเวกเตอร์ที่มีอยู่ได้อีกด้วย:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

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


1
โหวตให้คำตอบที่ดีที่สุดและมีรายละเอียดมากขึ้น อย่างไรก็ตามในตัวแปร swap () ของคุณ ( สำหรับ C ++ 03 ที่ไม่มี NRVO ) คุณจะยังคงมีสำเนาตัวสร้างสำเนาหนึ่งชุดที่อยู่ภายใน f (): จากผลลัพธ์ตัวแปรไปยังวัตถุชั่วคราวที่ซ่อนอยู่ซึ่งจะถูกสลับเป็นmyvec ในที่สุด
JenyaKh

@JenyaKh: แน่นอนว่านั่นเป็นปัญหาด้านคุณภาพของการนำไปใช้งาน มาตรฐานไม่ได้กำหนดให้การใช้งาน C ++ 03 ต้องใช้ NRVO เช่นเดียวกับที่ไม่ต้องใช้ฟังก์ชัน inlining ความแตกต่างจากฟังก์ชันอินไลน์คือการอินไลน์จะไม่เปลี่ยนความหมายหรือโปรแกรมของคุณในขณะที่ NRVO ทำ รหัสพกพาต้องใช้งานได้โดยมีหรือไม่มี NRVO โค้ดที่ปรับให้เหมาะสมสำหรับการนำไปใช้งานเฉพาะ (และแฟล็กคอมไพเลอร์เฉพาะ) สามารถขอการรับประกันเกี่ยวกับ NRVO ได้ในเอกสารของการนำไปใช้งาน
Steve Jessop

3

ถึงเวลาโพสต์คำตอบเกี่ยวกับRVOแล้วก็เช่นกัน ...

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


1

สำนวน pre-C ++ 11 ทั่วไปคือการส่งผ่านการอ้างอิงไปยังวัตถุที่กำลังเติม

จากนั้นจะไม่มีการคัดลอกเวกเตอร์

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
นั่นไม่ใช่สำนวนใน C ++ 11 อีกต่อไป
Nawaz

1
@Nawaz ฉันเห็นด้วย ฉันไม่แน่ใจว่าแนวทางปฏิบัติที่ดีที่สุดใน SO เกี่ยวกับคำถามเกี่ยวกับ C ++ แต่ไม่ใช่เฉพาะ C ++ 11 ฉันสงสัยว่าฉันควรจะให้คำตอบ C ++ 11 กับนักเรียน C ++ 03 ตอบใครบางคนที่มีรหัสการผลิตที่ลึก คุณมีความเห็น?
Drew Dormann

7
อันที่จริงหลังจากการเปิดตัว C ++ 11 (ซึ่งมีอายุ 19 เดือน) ฉันถือว่าทุกคำถามเป็นคำถาม C ++ 11 เว้นแต่จะระบุไว้อย่างชัดเจนว่าเป็นคำถาม C ++ 03
Nawaz

1

ถ้าคอมไพลเลอร์สนับสนุน Named Return Value Optimization ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ) คุณสามารถส่งคืนเวกเตอร์ได้โดยตรงโดยระบุว่าไม่มี:

  1. เส้นทางที่แตกต่างกันส่งคืนวัตถุที่มีชื่อต่างกัน
  2. เส้นทางการส่งคืนหลายเส้นทาง (แม้ว่าอ็อบเจ็กต์ที่มีชื่อเดียวกันจะถูกส่งคืนในทุกเส้นทาง) พร้อมกับแนะนำสถานะ EH
  3. อ็อบเจ็กต์ที่ระบุชื่อที่ส่งคืนถูกอ้างอิงในบล็อก asm แบบอินไลน์

NRVO ปรับตัวสร้างสำเนาที่ซ้ำซ้อนและการเรียกตัวทำลายล้างและปรับปรุงประสิทธิภาพโดยรวม

ไม่ควรมีความแตกต่างอย่างแท้จริงในตัวอย่างของคุณ


0
vector<string> getseq(char * db_file)

และถ้าคุณต้องการพิมพ์บน main () คุณควรทำแบบวนซ้ำ

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

อาจจะดีพอ ๆ กับ "return by value" แต่ก็เป็นประเภทของรหัสที่สามารถนำไปสู่ข้อผิดพลาดได้ พิจารณาโปรแกรมต่อไปนี้:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • ถาม: จะเกิดอะไรขึ้นเมื่อดำเนินการข้างต้น ตอบ: coredump
  • ถาม: ทำไมคอมไพเลอร์ไม่จับผิด ตอบ: เนื่องจากโปรแกรมมีวากยสัมพันธ์แม้ว่าจะไม่ถูกต้องตามความหมายก็ตาม
  • ถาม: จะเกิดอะไรขึ้นหากคุณแก้ไข vecFunc () เพื่อส่งคืนข้อมูลอ้างอิง ตอบ: โปรแกรมทำงานจนเสร็จสมบูรณ์และสร้างผลลัพธ์ที่คาดหวัง
  • ถาม: ความแตกต่างคืออะไร? ตอบ: คอมไพลเลอร์ไม่จำเป็นต้องสร้างและจัดการวัตถุที่ไม่ระบุชื่อ โปรแกรมเมอร์ได้สั่งให้คอมไพเลอร์ใช้อ็อบเจ็กต์เดียวสำหรับตัววนซ้ำและสำหรับการกำหนดจุดสิ้นสุดแทนที่จะเป็นอ็อบเจ็กต์ที่แตกต่างกันสองอ็อบเจ็กต์ดังตัวอย่างที่ไม่สมบูรณ์

โปรแกรมที่ผิดพลาดข้างต้นจะระบุว่าไม่มีข้อผิดพลาดแม้ว่าจะมีใครใช้ตัวเลือกการรายงาน GNU g ++ -Wall -Wextra -Weffc ++

หากคุณต้องสร้างค่าสิ่งต่อไปนี้จะใช้แทนการเรียก vecFunc () สองครั้ง:

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

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


3
นี่เป็นตัวอย่างของสิ่งที่อาจผิดพลาดได้หากผู้ใช้ไม่คุ้นเคยกับ C ++ โดยทั่วไป คนที่คุ้นเคยกับภาษาที่ใช้ออบเจ็กต์เช่น. net หรือ javascript อาจจะคิดว่าเวกเตอร์สตริงถูกส่งผ่านเป็นตัวชี้เสมอดังนั้นในตัวอย่างของคุณจะชี้ไปที่วัตถุเดียวกันเสมอ vecfunc (). begin () และ vecfunc (). end () ไม่จำเป็นต้องตรงกับตัวอย่างของคุณเนื่องจากควรเป็นสำเนาของเวกเตอร์สตริง
Medran

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