การใช้ 'อัตโนมัติ' ของ C ++ 11 สามารถปรับปรุงประสิทธิภาพได้หรือไม่


230

ฉันเห็นได้ว่าทำไมautoประเภทใน C ++ 11 ช่วยปรับปรุงความถูกต้องและการบำรุงรักษา ฉันอ่านแล้วว่ามันยังสามารถปรับปรุงประสิทธิภาพได้ ( เกือบจะอัตโนมัติทุกครั้งโดย Herb Sutter) แต่ฉันพลาดคำอธิบายที่ดี

  • จะautoปรับปรุงประสิทธิภาพได้อย่างไร
  • ใครสามารถยกตัวอย่างได้บ้าง

5
ดูherbutter.com/2013/06/13/…ซึ่งพูดถึงการหลีกเลี่ยงการแปลงโดยปริยายโดยไม่ตั้งใจเช่นจากแกดเจ็ตไปเป็นวิดเจ็ต มันไม่ใช่ปัญหาทั่วไป
Jonathan Wakely

42
คุณยอมรับ“ ทำให้มีโอกาสน้อยกว่าที่จะลดราคาโดยไม่ตั้งใจ” เป็นการปรับปรุงประสิทธิภาพหรือไม่?
5gon12eder

1
ความสมบูรณ์ของการทำความสะอาดโค้ดในอนาคตเท่านั้นอาจ
Croll

เราต้องการคำตอบสั้น ๆ : ไม่ถ้าคุณเก่ง มันสามารถป้องกันข้อผิดพลาด 'noobish' C ++ มีเส้นโค้งการเรียนรู้ซึ่งฆ่าผู้ที่ไม่ทำมันเลย
Alec Teal

คำตอบ:


309

autoสามารถช่วยประสิทธิภาพการทำงานโดยการหลีกเลี่ยงการแปลงนัยเงียบ ตัวอย่างที่ฉันคิดว่าน่าสนใจมีดังต่อไปนี้

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

เห็นข้อบกพร่องหรือไม่ ที่นี่เราคิดว่าเราใช้ทุกรายการในแผนที่โดยการอ้างอิง const และใช้ช่วงใหม่สำหรับการแสดงออกเพื่อทำให้เจตนาของเราชัดเจน แต่จริงๆแล้วเรากำลังคัดลอกทุกองค์ประกอบ นี้เป็นเพราะstd::map<Key, Val>::value_typeเป็นไม่ได้std::pair<const Key, Val> std::pair<Key, Val>ดังนั้นเมื่อเรา (โดยนัย) มี:

std::pair<Key, Val> const& item = *iter;

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

int const& i = 2.0; // perfectly OK

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

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(แน่นอนว่าไม่มี__tmpวัตถุจริงๆมันมีไว้เพื่อประกอบภาพเท่านั้นในความเป็นจริงชั่วคราวที่ไม่มีชื่อจะถูกผูกไว้กับitemตลอดชีวิตของมัน)

เพิ่งเปลี่ยนเป็น:

for (auto const& item : m) {
    // do stuff
}

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


19
@Barry คุณช่วยอธิบายได้ไหมว่าทำไมคอมไพเลอร์จะทำสำเนาอย่างมีความสุขแทนที่จะบ่นเกี่ยวกับการพยายามทำตัวstd::pair<const Key, Val> const &เป็นstd::pair<Key, Val> const &? ใหม่สำหรับ C ++ 11 ไม่แน่ใจว่าระยะไกลและautoเล่นกับสิ่งนี้อย่างไร
Agop

@ Barry ขอบคุณสำหรับคำอธิบาย นั่นเป็นชิ้นส่วนที่ฉันขาดไป - ด้วยเหตุผลบางอย่างฉันคิดว่าคุณคงไม่มีการอ้างอิงถึงค่าคงที่ชั่วคราว แต่แน่นอนคุณสามารถ - มันจะหยุดอยู่ที่จุดสิ้นสุดของขอบเขต
Agop

@barry ฉันได้รับคุณ แต่ปัญหาคือว่าไม่มีคำตอบที่ครอบคลุมเหตุผลทั้งหมดในการใช้autoที่เพิ่มประสิทธิภาพ ดังนั้นฉันจะเขียนมันในคำพูดของฉันด้านล่าง
Yakk - Adam Nevraumont

38
ฉันยังไม่คิดว่านี่เป็นข้อพิสูจน์ได้ว่า " autoปรับปรุงประสิทธิภาพ" เป็นเพียงตัวอย่างที่ " autoช่วยป้องกันความผิดพลาดของโปรแกรมเมอร์ที่ทำลายประสิทธิภาพ" ฉันส่งว่ามีความแตกต่างที่ลึกซึ้ง แต่สำคัญระหว่างทั้งสอง ถึงกระนั้น +1
การแข่งขัน Lightness ใน Orbit

70

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

ตัวอย่างทั่วไปมาจาก (ab) โดยใช้std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

ด้วยcmp2และcmp3อัลกอริธึมทั้งหมดสามารถอินไลน์การเปรียบเทียบการเปรียบเทียบในขณะที่ถ้าคุณสร้างstd::functionวัตถุไม่เพียง แต่การโทรไม่สามารถ inline แต่คุณยังต้องผ่านการค้นหา polymorphic ในการลบภายในประเภทของฟังก์ชั่นการห่อหุ้ม

อีกตัวแปรในชุดรูปแบบนี้คือคุณสามารถพูดได้:

auto && f = MakeAThing();

นี่คือการอ้างอิงเสมอถูกผูกไว้กับค่าของนิพจน์การเรียกใช้ฟังก์ชันและไม่เคยสร้างวัตถุเพิ่มเติมใด ๆ หากคุณไม่ทราบชนิดค่าที่ส่งกลับของคุณอาจจะถูกบังคับให้สร้างวัตถุใหม่ (อาจจะเป็นชั่วคราว) T && f = MakeAThing()ผ่านสิ่งที่ต้องการ (ยิ่งไปกว่านั้นauto &&ยังสามารถใช้งานได้เมื่อชนิดส่งคืนไม่สามารถเคลื่อนย้ายได้และค่าส่งคืนจะเป็นค่า prvalue)


ดังนั้นนี่คือ "ประเภทการหลีกเลี่ยงการลบ" autoเหตุผลที่จะใช้ ตัวแปรอื่นของคุณคือ "หลีกเลี่ยงการทำสำเนาโดยไม่ตั้งใจ" แต่ต้องการการจัดแต่งใหม่ ทำไมautoให้ความเร็วคุณแค่พิมพ์ประเภทนั่น? (ฉันคิดว่าคำตอบคือ "คุณเข้าใจผิดและมันแปลงเสียงเงียบ ๆ ") ซึ่งทำให้มันเป็นตัวอย่างที่ไม่ได้รับการอธิบายที่ดีของคำตอบของ Barry ไม่? นั่นคือมีสองกรณีพื้นฐาน: อัตโนมัติเพื่อหลีกเลี่ยงการลบประเภทและอัตโนมัติเพื่อหลีกเลี่ยงข้อผิดพลาดประเภทเงียบที่แปลงโดยไม่ตั้งใจทั้งสองซึ่งมีค่าใช้จ่ายเวลาทำงาน
Yakk - Adam Nevraumont

2
"ไม่เพียง แต่จะไม่สามารถรับสายได้" - ทำไมจึงเป็นเช่นนั้น คุณหมายความว่าในบางสิ่งบางอย่างป้องกันหลักการโทรถูก devirtualized หลังจากที่ข้อมูลการวิเคราะห์การไหลถ้าเฉพาะด้านที่เกี่ยวข้องของstd::bind, std::functionและstd::stable_partitionได้รับการ inlined? หรือว่าในทางปฏิบัติไม่มีคอมไพเลอร์ C ++ จะอินไลน์อย่างจริงจังเพียงพอที่จะแยกแยะความยุ่งเหยิง?
Steve Jessop

@SteveJessop: ส่วนใหญ่หลัง - หลังจากที่คุณผ่านstd::functionConstructor มันจะซับซ้อนมากที่จะเห็นผ่านการโทรจริงโดยเฉพาะอย่างยิ่งกับการเพิ่มประสิทธิภาพฟังก์ชั่นขนาดเล็ก (ดังนั้นคุณไม่ต้องการ devirtualization จริง ๆ ) แน่นอนในหลักการทุกอย่างเป็นเหมือน ...
Kerrek SB

41

มีสองประเภทคือ

autoสามารถหลีกเลี่ยงการลบประเภท มีประเภทที่ไม่สามารถแก้ไขได้ (เช่น lambdas) และประเภทที่เกือบจะไม่มีชื่อ (เช่นผลลัพธ์ของstd::bindหรือเทมเพลตนิพจน์อื่น ๆ ที่คล้าย ๆ )

โดยไม่ต้องคุณสิ้นสุดมีการลบข้อมูลประเภทลงไปบางอย่างเช่นauto std::functionการลบประเภทมีค่าใช้จ่าย

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1มีการลบประเภทค่าใช้จ่าย - การจัดสรรฮีปที่เป็นไปได้ความยากในการอินไลน์และค่าใช้จ่ายการเรียกใช้ตารางฟังก์ชันเสมือน task2ไม่มีเลย แลมบ์ดาต้องการรูปแบบการหักอัตโนมัติหรือรูปแบบอื่น ๆ เพื่อจัดเก็บโดยไม่ลบประเภท ประเภทอื่น ๆ นั้นซับซ้อนจนพวกมันต้องการในทางปฏิบัติเท่านั้น

ประการที่สองคุณสามารถพิมพ์ผิด ในบางกรณีประเภทที่ไม่ถูกต้องจะทำงานได้อย่างสมบูรณ์แบบ แต่จะทำให้เกิดสำเนา

Foo const& f = expression();

ถ้าจะรวบรวมexpression()ผลตอบแทนBar const&หรือBarหรือแม้กระทั่งการBar&ที่สามารถสร้างขึ้นมาจากFoo Barชั่วคราวFooจะถูกสร้างจากนั้นผูกติดกับfและอายุการใช้งานของมันจะขยายออกไปจนกว่าfจะหายไป

โปรแกรมเมอร์อาจมีความหมายBar const& fและไม่ได้ตั้งใจจะทำสำเนาที่นั่น แต่ทำสำเนาโดยไม่คำนึงถึง

ตัวอย่างที่พบบ่อยที่สุดคือประเภทของ*std::map<A,B>::const_iteratorซึ่งstd::pair<A const, B> const&ไม่ใช่std::pair<A,B> const&แต่ข้อผิดพลาดเป็นหมวดหมู่ของข้อผิดพลาดที่เงียบประสิทธิภาพ คุณสามารถสร้างจากstd::pair<A, B> std::pair<const A, B>(กุญแจบนแผนที่คือ const เพราะการแก้ไขมันเป็นความคิดที่ไม่ดี)

ทั้ง @Barry และ @KerrekSB ได้แสดงหลักการสองข้อนี้เป็นครั้งแรกในคำตอบของพวกเขา นี่เป็นเพียงความพยายามที่จะเน้นประเด็นทั้งสองในหนึ่งคำตอบด้วยถ้อยคำที่มุ่งเน้นที่ปัญหาแทนที่จะเป็นตัวอย่าง


9

คำตอบสามคำตอบที่มีอยู่ให้ตัวอย่างที่การใช้autoช่วย"ทำให้มีโอกาสน้อยที่จะทำให้ลดราคาโดยไม่ตั้งใจ" ได้อย่างมีประสิทธิภาพทำให้ "ปรับปรุงประสิทธิภาพ"

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

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

ส่งผลให้ผลผลิตที่แตกต่างกัน เป็นที่ยอมรับว่าส่วนใหญ่เป็นผลมาจากการประเมิน Eigens ขี้เกียจ แต่รหัสนั้น / ควรจะโปร่งใสกับผู้ใช้ (ห้องสมุด)

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


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