ตัวเลือกการคัดลอกและการเพิ่มประสิทธิภาพค่าตอบแทนคืออะไร?


377

การคัดลอกข้อมูลคืออะไร? การเพิ่มประสิทธิภาพค่าที่ส่งคืนคืออะไร พวกเขาหมายถึงอะไร

พวกเขาสามารถเกิดขึ้นได้ในสถานการณ์ใด ข้อ จำกัด คืออะไร

  • หากคุณได้รับการอ้างอิงกับคำถามนี้คุณอาจจะมองหาการแนะนำ
  • สำหรับภาพรวมทางเทคนิคดูมาตรฐานอ้างอิง
  • ดูกรณีทั่วไปที่นี่

1
การคัดลอกข้อมูลเป็นวิธีหนึ่งในการดู การแยกวัตถุหรือการรวมวัตถุ (หรือความสับสน) เป็นอีกมุมมองหนึ่ง
curiousguy

ฉันพบลิงค์นี้มีประโยชน์
subtleseeker

คำตอบ:


246

บทนำ

สำหรับภาพรวมทางเทคนิค - ข้ามไปที่คำตอบนี้

สำหรับกรณีทั่วไปที่มีการคัดลอกเกิดขึ้น - ข้ามไปที่คำตอบนี้

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

มันเป็นเพียงรูปแบบของการเพิ่มประสิทธิภาพที่ elides (ฮ่า!) ตามที่ถ้ากฎ - สำเนาตัดออกสามารถนำไปใช้แม้ว่าการคัดลอก / ย้ายวัตถุที่มีผลข้างเคียง

ตัวอย่างต่อไปนี้นำมาจากWikipedia :

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

ทั้งนี้ขึ้นอยู่กับคอมไพเลอร์และการตั้งค่าเอาท์พุทต่อไปนี้จะถูกต้องทั้งหมด :

สวัสดีชาวโลก!
ทำสำเนาแล้ว
ทำสำเนาแล้ว


สวัสดีชาวโลก!
ทำสำเนาแล้ว


สวัสดีชาวโลก!

สิ่งนี้ยังหมายถึงวัตถุที่สามารถสร้างได้น้อยลงดังนั้นคุณจึงไม่สามารถพึ่งพาจำนวน destructors ที่ระบุได้ คุณไม่ควรมีตรรกะที่สำคัญในการคัดลอก / ย้าย - คอนสตรัคเตอร์หรือ destructors เนื่องจากคุณไม่สามารถพึ่งพาพวกเขาถูกเรียก

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

C ++ 17 : ตั้งแต่ C ++ 17 รับประกันการคัดลอก Elision เมื่อวัตถุถูกส่งคืนโดยตรง:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}

2
คุณช่วยอธิบายได้ว่าเมื่อไหร่จะเกิดผลลัพธ์ที่ 2 และที่ 3?
zhangxaochen

3
@zhangxaochen เมื่อใดและอย่างไรคอมไพเลอร์ตัดสินใจที่จะปรับวิธีการให้เหมาะสม
Luchian Grigore

10
@zhangxaochen เอาต์พุต 1: copy 1 มาจากการกลับสู่ temp และคัดลอก 2 จาก temp ไปยัง obj; ที่สองคือเมื่อหนึ่งในข้างต้นถูก optimezed อาจคัดลอก reutnr ถูก elided; thris ทั้งสองจะ elided
ชนะ

2
อืม แต่ในความคิดของฉันนี้ต้องเป็นคุณลักษณะที่เราสามารถเชื่อถือได้ เพราะถ้าเราทำไม่ได้มันจะส่งผลอย่างรุนแรงต่อวิธีที่เราใช้งานฟังก์ชั่นของเราใน C ++ สมัยใหม่ (RVO vs std :: move) ระหว่างที่ดูวิดีโอ CppCon 2014 บางส่วนฉันได้รับความประทับใจจริง ๆ ว่าคอมไพเลอร์สมัยใหม่ทั้งหมดทำ RVO นอกจากนี้ฉันได้อ่านบางที่ที่ไม่มีการเพิ่มประสิทธิภาพใด ๆ คอมไพเลอร์ใช้มัน แต่แน่นอนฉันไม่แน่ใจเกี่ยวกับเรื่องนี้ นั่นเป็นเหตุผลที่ฉันถาม
j00hi

8
@ j00hi: อย่าเขียนการย้ายในคำสั่ง return - หากไม่ได้ใช้ rvo ค่าส่งคืนจะถูกย้ายออกไปตามค่าเริ่มต้น
MikeMB

96

การอ้างอิงมาตรฐาน

สำหรับมุมมองทางเทคนิค & การแนะนำน้อย - ข้ามไปที่คำตอบนี้

สำหรับกรณีทั่วไปที่มีการคัดลอกเกิดขึ้น - ข้ามไปที่คำตอบนี้

ตัวเลือกคัดลอกถูกกำหนดไว้ในมาตรฐานใน:

12.8 การคัดลอกและเคลื่อนย้ายวัตถุคลาส [class.copy]

เช่น

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

- ในคำสั่ง return ในฟังก์ชั่นที่มีประเภทคืนคลาสเมื่อนิพจน์เป็นชื่อของวัตถุอัตโนมัติที่ไม่ลบเลือน (นอกเหนือจากฟังก์ชั่นหรือพารามิเตอร์ catch-clause) ที่มีชนิด cvunqualified เช่นเดียวกับชนิดส่งคืนฟังก์ชัน การดำเนินการคัดลอก / ย้ายสามารถละเว้นได้โดยการสร้างวัตถุอัตโนมัติโดยตรงในค่าตอบแทนของฟังก์ชั่น

- ในการแสดงออกเมื่อตัวถูกดำเนินการเป็นชื่อของวัตถุอัตโนมัติที่ไม่ลบเลือน (นอกเหนือจากฟังก์ชั่นหรือพารามิเตอร์ catch-clause) ที่มีขอบเขตไม่ขยายเกินกว่าจุดสิ้นสุดของการลองบล็อกในสุด (ถ้ามี หนึ่ง) การดำเนินการคัดลอก / ย้ายจากตัวถูกดำเนินการไปยังวัตถุข้อยกเว้น (15.1) สามารถละเว้นได้โดยการสร้างวัตถุอัตโนมัติโดยตรงในวัตถุข้อยกเว้น

- เมื่อวัตถุคลาสชั่วคราวที่ไม่ได้ถูกผูกไว้กับการอ้างอิง (12.2) จะถูกคัดลอก / ย้ายไปยังวัตถุคลาสที่มีชนิด CV-unqualified เดียวกันการดำเนินการคัดลอก / ย้ายสามารถละเว้นได้โดยการสร้างวัตถุชั่วคราวโดยตรงใน เป้าหมายของการคัดลอก / ย้ายที่ถูกละไว้

- เมื่อข้อยกเว้นการประกาศของตัวจัดการข้อยกเว้น (ข้อ 15) ประกาศวัตถุประเภทเดียวกัน (ยกเว้นคุณสมบัติ CVV) เป็นวัตถุยกเว้น (15.1) การดำเนินการคัดลอก / ย้ายสามารถละเว้นได้โดยการปฏิบัติกับข้อยกเว้นประกาศ เป็นนามแฝงสำหรับวัตถุข้อยกเว้นหากความหมายของโปรแกรมจะไม่เปลี่ยนแปลงยกเว้นการดำเนินการของตัวสร้างและ destructors สำหรับวัตถุที่ประกาศโดยการประกาศข้อยกเว้น

123) เนื่องจากมีเพียงวัตถุเดียวเท่านั้นที่ถูกทำลายแทนที่จะเป็นสองวัตถุและตัวสร้างสำเนา / ย้ายไม่ถูกดำเนินการจึงยังมีวัตถุหนึ่งชิ้นที่ถูกทำลายสำหรับแต่ละสิ่งก่อสร้าง

ตัวอย่างที่ให้มาคือ:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

และอธิบาย:

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


1
นั่นมาจากมาตรฐาน C ++ 17 หรือจากรุ่นก่อนหน้าใช่ไหม
Nils

90

คัดลอกรูปแบบทั่วไป

สำหรับภาพรวมทางเทคนิค - ข้ามไปที่คำตอบนี้

สำหรับมุมมองทางเทคนิค & การแนะนำน้อย - ข้ามไปที่คำตอบนี้

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

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

การปรับค่าส่งคืนตามปกติเกิดขึ้นเมื่อมีการส่งคืนชั่วคราว:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

สถานที่ทั่วไปอื่น ๆ ที่เกิดการคัดลอกเกิดขึ้นคือเมื่อมีการส่งค่าชั่วคราวโดย :

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

หรือเมื่อมีการโยนข้อยกเว้นและตามค่า :

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

ข้อ จำกัด ทั่วไปของการคัดลอกสำเนาคือ:

  • หลายจุดกลับมา
  • การเริ่มต้นตามเงื่อนไข

คอมไพเลอร์เกรดเชิงพาณิชย์ส่วนใหญ่รองรับการคัดลอกตัวเลือก & (N) RVO (ขึ้นอยู่กับการตั้งค่าการปรับให้เหมาะสม)


4
ฉันสนใจที่จะเห็นสัญลักษณ์แสดงหัวข้อ "ข้อ จำกัด ทั่วไป" อธิบายเพียงเล็กน้อย ... อะไรทำให้ปัจจัย จำกัด เหล่านี้คืออะไร
โทรศัพท์

@ phonetagger ฉันเชื่อมโยงกับบทความ msdn หวังว่าจะกำจัดสิ่งต่างๆออก
Luchian Grigore

54

Copy elision เป็นเทคนิคการปรับแต่งคอมไพเลอร์ที่กำจัดการคัดลอก / ย้ายวัตถุโดยไม่จำเป็น

ในสถานการณ์ต่อไปนี้คอมไพเลอร์ได้รับอนุญาตให้ละเว้นการดำเนินการคัดลอก / ย้ายและด้วยเหตุนี้จะไม่เรียกตัวสร้างที่เกี่ยวข้อง:

  1. NRVO (Named Return Value Optimization) : หากฟังก์ชั่นส่งคืนชนิดคลาสตามค่าและนิพจน์ของคำสั่ง return เป็นชื่อของวัตถุที่ไม่ลบเลือนพร้อมระยะเวลาการจัดเก็บอัตโนมัติ (ซึ่งไม่ใช่พารามิเตอร์ฟังก์ชัน) ดังนั้นการคัดลอก / ย้าย ที่จะดำเนินการโดยคอมไพเลอร์ไม่เพิ่มประสิทธิภาพสามารถละเว้น หากเป็นเช่นนั้นค่าที่ส่งคืนจะถูกสร้างขึ้นโดยตรงในหน่วยเก็บข้อมูลที่ค่าส่งคืนของฟังก์ชันจะถูกย้ายหรือคัดลอก
  2. RVO (Return Value Optimization) : หากฟังก์ชั่นส่งคืนวัตถุชั่วคราวที่ไม่ระบุชื่อที่จะย้ายหรือคัดลอกไปยังปลายทางโดยคอมไพเลอร์ไร้เดียงสาการคัดลอกหรือย้ายสามารถละเว้นได้ตาม 1
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

แม้เมื่อการคัดลอกเกิดขึ้นและไม่มีการเรียก copy- / move-constructor จะต้องมีอยู่และสามารถเข้าถึงได้ (ราวกับว่าไม่มีการเพิ่มประสิทธิภาพเกิดขึ้นเลย) มิฉะนั้นโปรแกรมจะถูกจัดรูปแบบไม่ถูกต้อง

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

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

GCC มี-fno-elide-constructorsตัวเลือกให้ปิดการใช้งานการคัดลอกข้อมูล -fno-elide-constructorsหากคุณต้องการที่จะหลีกเลี่ยงการตัดออกสำเนาที่เป็นไปได้ใช้

ตอนนี้คอมไพเลอร์เกือบทั้งหมดจะมีตัวเลือกการคัดลอกเมื่อเปิดใช้งานการปรับให้เหมาะสมที่สุด

ข้อสรุป

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


6
คำสั่ง ABC obj2(xyz123());มันคือ NRVO หรือ RVO? มันไม่ได้รับตัวแปร / วัตถุชั่วคราวเช่นเดียวกับ ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq

3
ในการมีภาพประกอบที่เป็นรูปธรรมมากขึ้นของ RVO คุณสามารถอ้างถึงชุดประกอบที่คอมไพเลอร์สร้างขึ้น (เปลี่ยนธงคอมไพเลอร์ -fno-elide-constructors เพื่อดู diff) godbolt.org/g/Y2KcdH
Gab 是好人
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.