C ++ ที่ทันสมัยช่วยเพิ่มประสิทธิภาพให้คุณได้ฟรีหรือไม่?


205

บางครั้งมีการอ้างว่า C ++ 11/14 สามารถเพิ่มประสิทธิภาพให้คุณได้แม้เพียงแค่รวบรวมรหัส C ++ 98 การให้เหตุผลมักตามแนวความหมายของการย้ายเนื่องจากในบางกรณีตัวสร้าง rvalue จะถูกสร้างขึ้นโดยอัตโนมัติหรือตอนนี้เป็นส่วนหนึ่งของ STL ตอนนี้ฉันสงสัยว่าก่อนหน้านี้กรณีเหล่านี้ได้รับการจัดการโดย RVO หรือการเพิ่มประสิทธิภาพคอมไพเลอร์ที่คล้ายกันแล้ว

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

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


3
โปรดจำไว้ว่าการทำสำเนาการคัดลอกและการปรับค่าส่งคืนจะดำเนินการเมื่อสร้างวัตถุใหม่โดยใช้ตัวสร้างสำเนา อย่างไรก็ตามในตัวดำเนินการกำหนดค่าการคัดลอกไม่มีตัวเลือกการคัดลอก (เป็นไปได้อย่างไรเนื่องจากคอมไพเลอร์ไม่ทราบว่าจะทำอย่างไรกับวัตถุที่สร้างขึ้นแล้วซึ่งไม่ใช่ชั่วคราว) ดังนั้นในกรณีนี้ C ++ 11/14 ชนะใหญ่โดยให้ความเป็นไปได้ในการใช้ตัวดำเนินการกำหนดค่าการย้าย เกี่ยวกับคำถามของคุณฉันไม่คิดว่ารหัส C ++ 98 ควรจะเร็วกว่านี้หากคอมไพเลอร์ด้วยคอมไพเลอร์ C ++ 11/14 บางทีมันอาจเร็วกว่าเพราะคอมไพเลอร์ใหม่กว่า
vsoftco

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

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

เรียนไม่จำเป็นต้องง่ายเพื่อที่จะไม่มีตัวสร้างสำเนา C ++ เจริญเติบโตได้ในความหมายของค่าและการคัดลอกคอนสตรัคผู้ประกอบการที่ได้รับมอบหมาย destructor ฯลฯ ควรเป็นข้อยกเว้น
sp2danny

1
@Eric ขอบคุณสำหรับลิงค์มันน่าสนใจ อย่างไรก็ตามเมื่อมองผ่านไปอย่างรวดเร็วข้อดีของความเร็วในการใช้งานนั้นส่วนใหญ่มาจากการเพิ่มstd::moveและเคลื่อนย้ายคอนสตรัคเตอร์ (ซึ่งจำเป็นต้องมีการแก้ไขโค้ดที่มีอยู่) สิ่งเดียวที่เกี่ยวข้องกับคำถามของฉันคือประโยคที่ว่า "คุณได้รับผลประโยชน์จากความเร็วที่รวดเร็วเพียงแค่ทำการคอมไพล์ใหม่" ซึ่งไม่ได้รับการสนับสนุนจากตัวอย่างใด ๆ (มันพูดถึง STL ในสไลด์เดียวกับที่ฉันทำ ) ฉันกำลังขอตัวอย่าง หากฉันอ่านสไลด์ผิดให้ฉันรู้
alarge

คำตอบ:


221

ฉันรับรู้ถึง 5 หมวดหมู่ทั่วไปที่คอมไพล์คอมไพเลอร์ C ++ 03 อีกครั้งเนื่องจาก C ++ 11 สามารถทำให้ประสิทธิภาพเพิ่มขึ้นอย่างไม่ จำกัด ซึ่งไม่เกี่ยวข้องกับคุณภาพของการใช้งานจริง นี่คือความหมายของการย้าย

std::vector กำหนดใหม่

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

เวลาทุกfoo's บัฟเฟอร์จะจัดสรรใน C ++ 03 มันคัดลอกทุกในvectorbar

ใน C ++ 11 แทนที่จะเป็นการย้ายbar::datas ซึ่งโดยพื้นฐานแล้วจะว่าง

ในกรณีนี้นี้ต้องอาศัยการเพิ่มประสิทธิภาพภายในภาชนะstd vectorในทุกกรณีด้านล่างการใช้stdคอนเทนเนอร์เป็นเพราะมันเป็นวัตถุ C ++ ที่มีmoveความหมายที่มีประสิทธิภาพใน C ++ 11 "อัตโนมัติ" เมื่อคุณอัพเกรดคอมไพเลอร์ของคุณ วัตถุที่ไม่ได้บล็อกมันที่มีstdคอนเทนเนอร์ยังสืบทอดตัวmoveสร้างการปรับปรุงอัตโนมัติ

ความล้มเหลวของ NRVO

เมื่อ NRVO (การเพิ่มประสิทธิภาพค่าที่ส่งคืนที่ระบุชื่อ) ล้มเหลวใน C ++ 03 จะกลับไปคัดลอกบน C ++ 11 จะกลับไปที่การย้าย ความล้มเหลวของ NRVO นั้นง่าย:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

หรือแม้กระทั่ง:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

เรามีสามค่า - ค่าส่งคืนและสองค่าที่แตกต่างกันภายในฟังก์ชั่น Elision อนุญาตให้ค่าในฟังก์ชั่น 'รวม' กับค่าส่งคืน แต่ไม่ได้อยู่ด้วยกัน ทั้งสองไม่สามารถผสานกับค่าส่งคืนโดยไม่รวมเข้าด้วยกัน

ปัญหาพื้นฐานคือ NRision elision มีความเปราะบางและรหัสที่มีการเปลี่ยนแปลงที่ไม่ได้อยู่ใกล้returnไซต์สามารถทำให้ประสิทธิภาพลดลงอย่างมากที่จุดนั้นโดยไม่มีการวินิจฉัยออกมา ในกรณีที่ความล้มเหลวของ NRVO ส่วนใหญ่ C ++ 11 จบลงด้วย a moveในขณะที่ C ++ 03 จบลงด้วยการคัดลอก

ส่งคืนอาร์กิวเมนต์ของฟังก์ชัน

Elision เป็นไปไม่ได้ที่นี่:

std::set<int> func(std::set<int> in){
  return in;
}

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

อย่างไรก็ตาม C ++ 11 สามารถย้ายจากที่หนึ่งไปอีกที่หนึ่ง (ในตัวอย่างของเล่นที่น้อยกว่าอาจมีบางสิ่งที่ทำให้set)

push_back หรือ insert

ในที่สุดการตัดลงในคอนเทนเนอร์จะไม่เกิดขึ้น: แต่ C ++ 11 โอเวอร์โหลด rvalue ย้ายตัวดำเนินการแทรกซึ่งบันทึกสำเนา

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

ใน C ++ 03 ชั่วคราวถูกสร้างขึ้นแล้วมันจะคัดลอกลงในเวกเตอร์whatever มีการจัดสรรบัฟเฟอร์v2 ตัวstd::stringโดยแต่ละชุดมีข้อมูลเหมือนกันและอีกหนึ่งตัวจะถูกทิ้ง

ใน C ++ 11 whateverจะมีการสร้างชั่วคราว whatever&& push_backเกินแล้วmoves vที่ชั่วคราวลงในเวกเตอร์ std::stringมีการจัดสรรบัฟเฟอร์หนึ่งรายการและย้ายไปยังเวกเตอร์ ว่างเปล่าstd::stringถูกทิ้ง

การมอบหมาย

ถูกขโมยจากคำตอบของ @ Jarod42 ด้านล่าง

Elision ไม่สามารถเกิดขึ้นได้กับการมอบหมาย แต่ย้ายจากสามารถ

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

ที่นี่some_functionส่งกลับผู้สมัครที่จะหลบหนี แต่เนื่องจากมันไม่ได้ใช้เพื่อสร้างวัตถุโดยตรงจึงไม่สามารถถูกตัดออกได้ ใน C ++ 03 some_valueผลดังกล่าวข้างต้นในเนื้อหาของความเป็นอยู่ชั่วคราวคัดลอกลง ใน C ++ 11 จะถูกย้ายเข้าsome_valueซึ่งโดยทั่วไปจะว่าง


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

MSVC 2013 ใช้ตัวสร้างย้ายในstdคอนเทนเนอร์ แต่ไม่สังเคราะห์ตัวสร้างย้ายในประเภทของคุณ

ดังนั้นประเภทที่มีstd::vectors และที่คล้ายกันจะไม่ได้รับการปรับปรุงใน MSVC2013 แต่จะเริ่มได้ใน MSVC2015

เสียงดังกราวและ gcc มีความยาวตั้งแต่นำไปใช้กับตัวสร้างการเคลื่อนย้ายโดยนัย อินเทล 2013 คอมไพเลอร์จะให้การสนับสนุนการสร้างโดยนัยของการก่อสร้างย้ายถ้าคุณผ่าน-Qoption,cpp,--gen_move_operations(พวกเขาไม่ได้ทำมันโดยเริ่มต้นในความพยายามที่จะข้ามเข้ากันได้กับ MSVC2013 เป็นพิเศษ)


1
@ ขนาดใหญ่ใช่ แต่สำหรับตัวสร้างการย้ายจะมีประสิทธิภาพมากกว่าตัวสร้างการคัดลอกหลายครั้งโดยปกติแล้วจะต้องย้ายทรัพยากรแทนที่จะคัดลอก โดยไม่ต้องเขียนตัวสร้างการย้ายของคุณเอง (และเพียงแค่คอมไพล์โปรแกรม C ++ 03 อีกครั้ง) stdไลบรารีคอนเทนเนอร์จะถูกอัพเดตด้วยตัวmoveสร้าง "ฟรี" และ (ถ้าคุณไม่ได้บล็อก) โครงสร้างที่ใช้วัตถุดังกล่าว ( และวัตถุที่กล่าวมา) จะเริ่มก่อสร้างสิ่งก่อสร้างฟรีในหลาย ๆ สถานการณ์ หลายสถานการณ์เหล่านั้นถูกครอบคลุมโดย elision ใน C ++ 03: ไม่ใช่ทั้งหมด
Yakk - Adam Nevraumont

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

2
@alarge มีสถานที่ที่ elision ล้มเหลวเช่นเมื่อวัตถุสองชิ้นที่มีช่วงชีวิตซ้อนทับกันสามารถนำไปรวมกันเป็นหนึ่งในสามได้ แต่ไม่ใช่ซึ่งกันและกัน จำเป็นต้องมีการย้ายใน C ++ 11 และคัดลอกใน C ++ 03 (ไม่ต้องสนใจ) Elision มักเปราะบางในทางปฏิบัติ การใช้stdภาชนะด้านบนส่วนใหญ่เป็นเพราะพวกเขามีราคาถูกที่จะย้าย exoensive เพื่อคัดลอกประเภทที่คุณได้รับ 'ฟรี' ใน C ++ 11 เมื่อ recompiling C ++ 03 vector::resizeเป็นข้อยกเว้น: จะใช้moveใน C ++ 11
Yakk - Adam Nevraumont

27
ฉันเห็นเพียงหมวดหมู่ทั่วไป 1 หมวดซึ่งเป็นการย้ายซีแมนทิกส์และอีก 5 กรณีพิเศษ
Johannes Schaub - litb

3
@ sebro ฉันเข้าใจคุณไม่พิจารณา "ทำให้โปรแกรมไม่จัดสรร 1,000s ของการจัดสรรกิโลไบต์จำนวนมากและย้ายพอยน์เตอร์ไปรอบ ๆ " ให้เพียงพอ คุณต้องการผลลัพธ์ตามกำหนดเวลา Microbenchmarks ไม่ได้เป็นเครื่องพิสูจน์การปรับปรุงประสิทธิภาพมากกว่าการพิสูจน์ว่าคุณกำลังทำอะไรอยู่น้อยลง แอปพลิเคชันในโลกแห่งความจริงเพียงไม่กี่ 100 ตัวในอุตสาหกรรมหลากหลายประเภทที่ถูกทำโปรไฟล์ด้วยการทำโปรไฟล์ในโลกแห่งความเป็นจริง ฉันอ้างสิทธิ์คลุมเครือเกี่ยวกับ "ประสิทธิภาพฟรี" และทำให้พวกเขามีข้อมูลเฉพาะเกี่ยวกับความแตกต่างในพฤติกรรมของโปรแกรมภายใต้ C ++ 03 และ C ++ 11
Yakk - Adam Nevraumont

46

หากคุณมีสิ่งที่ชอบ:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

คุณได้รับสำเนาใน C ++ 03 ในขณะที่คุณได้รับมอบหมายการย้ายใน C ++ 11 ดังนั้นคุณมีการเพิ่มประสิทธิภาพฟรีในกรณีนั้น


4
@Yakk: การคัดลอกเกิดขึ้นได้อย่างไรในการมอบหมาย?
Jarod42

2
@ Jarod42 ฉันเชื่อว่าการคัดลอกข้อมูลเป็นไปไม่ได้ในการมอบหมายเนื่องจากทางด้านซ้ายมือถูกสร้างขึ้นแล้วและไม่มีวิธีที่สมเหตุสมผลที่คอมไพเลอร์จะรู้ว่าจะทำอย่างไรกับข้อมูล "เก่า" หลังจากขโมยทรัพยากรจากด้านขวา ด้านซ้ายมือ. แต่บางทีฉันผิดฉันชอบที่จะหาคำตอบครั้งเดียวและตลอดไป การคัดลอกทำให้เหมาะสมเมื่อคุณคัดลอกโครงสร้างเนื่องจากวัตถุเป็น "สด" และไม่มีปัญหาในการตัดสินใจว่าจะทำอย่างไรกับข้อมูลเก่า เท่าที่ฉันทราบข้อยกเว้นเพียงอย่างเดียวคือ: "การมอบหมายสามารถถูกลบออกได้โดยยึดตามกฎถ้า"
vsoftco

4
รหัส C ++ 03 ที่ดีก็มีการเคลื่อนไหวในกรณีนี้ผ่านทางfoo().swap(v);
เบ็น Voigt

@BenVoigt แน่ใจ แต่ไม่ใช่รหัสทั้งหมดที่ได้รับการปรับปรุงและไม่ใช่ทุกจุดที่เกิดเหตุการณ์นี้ง่ายต่อการเข้าถึง
Yakk - Adam Nevraumont

ellision การคัดลอกสามารถทำงานในการมอบหมายเช่น @BenVoigt พูดว่า คำที่ดีกว่าคือ RVO (การเพิ่มประสิทธิภาพค่าที่ส่งคืน) และใช้ได้เฉพาะเมื่อ foo () ถูกนำไปใช้เช่นนั้น
DrumM
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.