อันที่จริงตั้งแต่ 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);
:
ข้อสรุป
- การใช้พารามิเตอร์เอาต์พุตแทนการส่งคืนตามค่าอาจให้ประสิทธิภาพที่เพิ่มขึ้นจากการใช้ความจุซ้ำ
- ในคอมพิวเตอร์เดสก์ท็อปสมัยใหม่ดูเหมือนว่าจะใช้ได้กับเวกเตอร์ขนาดใหญ่ (> 16MB) และเวกเตอร์ขนาดเล็ก (<1kB) เท่านั้น
- หลีกเลี่ยงการจัดสรรเวกเตอร์ขนาดเล็กหลายล้าน / พันล้าน (<1kB) ถ้าเป็นไปได้ให้ใช้ความจุซ้ำหรือดีกว่าให้ออกแบบสถาปัตยกรรมของคุณให้แตกต่างออกไป
ผลลัพธ์อาจแตกต่างกันไปในแพลตฟอร์มอื่น ตามปกติหากประสิทธิภาพมีความสำคัญให้เขียนเกณฑ์มาตรฐานสำหรับกรณีการใช้งานเฉพาะของคุณ