เมื่อเร็ว ๆ นี้ฉันได้ใช้เกณฑ์มาตรฐานเกี่ยวกับโครงสร้างข้อมูลที่แตกต่างกันที่ บริษัท ของฉันดังนั้นฉันจึงรู้สึกว่าจำเป็นต้องพูดอะไรสักอย่าง การเปรียบเทียบบางสิ่งให้ถูกต้องมีความซับซ้อนมาก
การเปรียบเทียบ
บนเว็บเราแทบไม่พบ (ถ้าเคย) เกณฑ์มาตรฐานที่ออกแบบมาอย่างดี จนถึงวันนี้ฉันพบเพียงเกณฑ์มาตรฐานที่ทำแบบนักข่าวเท่านั้น (ค่อนข้างเร็วและกวาดตัวแปรหลายสิบตัวไว้ใต้พรม)
1)คุณต้องพิจารณาเกี่ยวกับการอุ่นแคช
คนส่วนใหญ่ที่ใช้เกณฑ์มาตรฐานมักกลัวความคลาดเคลื่อนของตัวจับเวลาดังนั้นพวกเขาจึงเรียกใช้สิ่งต่างๆเป็นพัน ๆ ครั้งและใช้เวลาตลอดเวลาพวกเขาเพียงแค่ระมัดระวังที่จะใช้เวลาหลายพันเท่าสำหรับการดำเนินการทุกครั้งจากนั้นจึงพิจารณาสิ่งที่เทียบเคียงได้
ความจริงก็คือในโลกแห่งความเป็นจริงมันมีเหตุผลเล็กน้อยเพราะแคชของคุณจะไม่อุ่นและการทำงานของคุณจะถูกเรียกเพียงครั้งเดียว ดังนั้นคุณต้องเปรียบเทียบโดยใช้ RDTSC และเวลาเรียกสิ่งเหล่านี้เพียงครั้งเดียว Intel ได้จัดทำเอกสารอธิบายวิธีการใช้ RDTSC (โดยใช้คำสั่ง cpuid เพื่อล้างไปป์ไลน์และเรียกใช้อย่างน้อย 3 ครั้งที่จุดเริ่มต้นของโปรแกรมเพื่อทำให้เสถียร)
2) การวัดความถูกต้องของ RDTSC
ฉันขอแนะนำให้ทำสิ่งนี้:
u64 g_correctionFactor; // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;
static u64 const errormeasure = ~((u64)0);
#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // flush OOO instruction pipeline
return __rdtsc();
}
inline void WarmupRDTSC()
{
int a[4];
__cpuid(a, 0x80000000); // warmup cpuid.
__cpuid(a, 0x80000000);
__cpuid(a, 0x80000000);
// measure the measurer overhead with the measurer (crazy he..)
u64 minDiff = LLONG_MAX;
u64 maxDiff = 0; // this is going to help calculate our PRECISION ERROR MARGIN
for (int i = 0; i < 80; ++i)
{
u64 tick1 = GetRDTSC();
u64 tick2 = GetRDTSC();
minDiff = std::min(minDiff, tick2 - tick1); // make many takes, take the smallest that ever come.
maxDiff = std::max(maxDiff, tick2 - tick1);
}
g_correctionFactor = minDiff;
printf("Correction factor %llu clocks\n", g_correctionFactor);
g_accuracy = maxDiff - minDiff;
printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif
นี่เป็นตัววัดความคลาดเคลื่อนและจะใช้ค่าต่ำสุดของค่าที่วัดได้ทั้งหมดเพื่อหลีกเลี่ยงการได้รับ a -10 ** 18 (ค่าเชิงลบ 64 บิตแรก) เป็นครั้งคราว
สังเกตการใช้อินทรินซิคไม่ใช่การประกอบแบบอินไลน์ คอมไพเลอร์แบบอินไลน์แรกแทบไม่ได้รับการสนับสนุนในปัจจุบัน แต่ที่แย่กว่านั้นคือคอมไพเลอร์สร้างอุปสรรคในการสั่งซื้อเต็มรูปแบบรอบ ๆ แอสเซมบลีแบบอินไลน์เนื่องจากไม่สามารถวิเคราะห์ด้านในแบบคงที่ได้ดังนั้นนี่จึงเป็นปัญหาในการเปรียบเทียบสิ่งต่าง ๆ ในโลกแห่งความเป็นจริงโดยเฉพาะอย่างยิ่งเมื่อเรียกสิ่งต่างๆ ครั้งเดียว. ดังนั้น intrinsic จึงเหมาะกับที่นี่เพราะมันไม่ทำลายคอมไพเลอร์ที่สั่งซื้อใหม่ฟรี
3)พารามิเตอร์
ปัญหาสุดท้ายคือผู้คนมักจะทดสอบสถานการณ์ที่แตกต่างกันน้อยเกินไป ประสิทธิภาพของคอนเทนเนอร์ได้รับผลกระทบจาก:
- ผู้จัดสรร
- ขนาดของประเภทที่มีอยู่
- ค่าใช้จ่ายในการดำเนินการคัดลอกการดำเนินการมอบหมายการย้ายการดำเนินการก่อสร้างประเภทที่มีอยู่
- จำนวนองค์ประกอบในคอนเทนเนอร์ (ขนาดของปัญหา)
- ประเภทมี 3 การดำเนินการเล็กน้อย
- ประเภทคือ POD
จุดที่ 1 มีความสำคัญเนื่องจากคอนเทนเนอร์จะจัดสรรเป็นครั้งคราวและจะมีความสำคัญมากหากพวกเขาจัดสรรโดยใช้ CRT "ใหม่" หรือการดำเนินการที่ผู้ใช้กำหนดเองเช่นการจัดสรรพูลหรือฟรีลิสต์หรืออื่น ๆ ...
( สำหรับผู้ที่สนใจเกี่ยวกับ pt 1 เข้าร่วมเธรดลึกลับใน gamedevเกี่ยวกับผลกระทบด้านประสิทธิภาพของตัวจัดสรรระบบ )
จุดที่ 2 เป็นเพราะคอนเทนเนอร์บางตัว (พูดว่า A) จะเสียเวลาในการคัดลอกสิ่งต่างๆรอบตัวและยิ่งประเภทใหญ่ขึ้นค่าใช้จ่ายก็จะยิ่งใหญ่ขึ้น ปัญหาคือเมื่อเปรียบเทียบกับคอนเทนเนอร์ B อื่น A อาจชนะ B สำหรับประเภทเล็กและแพ้สำหรับประเภทที่ใหญ่กว่า
จุดที่ 3 เหมือนกับจุดที่ 2 ยกเว้นว่าจะคูณต้นทุนด้วยปัจจัยถ่วงน้ำหนัก
จุดที่ 4 เป็นคำถามเกี่ยวกับ O ขนาดใหญ่ที่ผสมกับปัญหาแคช คอนเทนเนอร์ที่มีความซับซ้อนไม่ดีบางส่วนสามารถทำงานได้ดีกว่าคอนเทนเนอร์ที่มีความซับซ้อนต่ำสำหรับประเภทจำนวนน้อย (เช่นmap
เทียบกับvector
เนื่องจากตำแหน่งของแคชเป็นสิ่งที่ดี แต่map
ทำให้หน่วยความจำแยกส่วน) จากนั้นเมื่อถึงจุดตัดบางจุดพวกเขาจะสูญเสียเนื่องจากขนาดโดยรวมที่มีอยู่เริ่ม "รั่ว" ไปยังหน่วยความจำหลักและทำให้แคชขาดหายไปบวกกับความจริงที่ว่าความซับซ้อนของ asymptotic สามารถเริ่มสัมผัสได้
จุดที่ 5 เป็นเรื่องเกี่ยวกับคอมไพเลอร์ที่สามารถกำจัดสิ่งที่ว่างเปล่าหรือไม่สำคัญในเวลาคอมไพล์ สิ่งนี้สามารถเพิ่มประสิทธิภาพการดำเนินการบางอย่างได้อย่างมากเนื่องจากคอนเทนเนอร์เป็นเทมเพลตดังนั้นแต่ละประเภทจะมีโปรไฟล์ประสิทธิภาพของตัวเอง
จุดที่ 6 เช่นเดียวกับจุดที่ 5 POD จะได้รับประโยชน์จากข้อเท็จจริงที่ว่าการสร้างสำเนาเป็นเพียง memcpy และคอนเทนเนอร์บางส่วนอาจมีการใช้งานเฉพาะสำหรับกรณีเหล่านี้โดยใช้ความเชี่ยวชาญพิเศษของเทมเพลตบางส่วนหรือ SFINAE เพื่อเลือกอัลกอริทึมตามลักษณะของ T.
เกี่ยวกับแผนที่แบน
เห็นได้ชัดว่าแผนที่แบบแบนเป็นเวคเตอร์ที่เรียงลำดับเช่น Loki AssocVector แต่ด้วยความทันสมัยเพิ่มเติมบางอย่างที่มาพร้อมกับ C ++ 11 การใช้ประโยชน์จากความหมายการเคลื่อนที่เพื่อเร่งการแทรกและลบองค์ประกอบเดี่ยว
นี่ยังคงเป็นตู้คอนเทนเนอร์ตามสั่ง unordered..
คนส่วนใหญ่มักจะไม่จำเป็นต้องสั่งซื้อส่วนหนึ่งดังนั้นการดำรงอยู่ของ
คุณคิดว่าบางทีคุณอาจต้องการflat_unorderedmap
? ซึ่งจะเป็นแบบนั้นgoogle::sparse_map
หรือแบบนั้น - แผนที่แฮชที่อยู่แบบเปิด
ปัญหาของแผนที่แฮชที่อยู่แบบเปิดคือในขณะที่rehash
พวกเขาต้องคัดลอกทุกอย่างรอบ ๆ ไปยังพื้นที่ราบที่ขยายใหม่ในขณะที่แผนที่ที่ไม่มีการเรียงลำดับมาตรฐานจะต้องสร้างดัชนีแฮชขึ้นใหม่ในขณะที่ข้อมูลที่จัดสรรจะยังคงอยู่ ข้อเสียของหลักสูตรคือความทรงจำที่กระจัดกระจายเหมือนนรก
เกณฑ์ของการ rehash ในแผนที่แฮชแอดเดรสแบบเปิดคือเมื่อความจุเกินขนาดของเวกเตอร์ที่เก็บข้อมูลคูณด้วยโหลดแฟกเตอร์
ปัจจัยโหลดทั่วไปคือ0.8
; ดังนั้นคุณต้องใส่ใจในเรื่องนี้หากคุณสามารถปรับขนาดแผนที่แฮชของคุณล่วงหน้าก่อนที่จะเติมได้ให้กำหนดขนาดล่วงหน้าไว้ที่: intended_filling * (1/0.8) + epsilon
สิ่งนี้จะทำให้คุณรับประกันได้ว่าจะไม่ต้องทำการซ่อมแซมและคัดลอกทุกอย่างอย่างปลอมแปลงในระหว่างการเติม
ข้อดีของแผนที่ที่อยู่แบบปิด ( std::unordered..
) คือคุณไม่ต้องสนใจพารามิเตอร์เหล่านั้น
แต่boost::flat_map
เป็นเวกเตอร์สั่ง ดังนั้นมันจะมีความซับซ้อนของ asymptotic log (N) เสมอซึ่งดีน้อยกว่าแผนที่แฮชแอดเดรสที่เปิดอยู่ (เวลาคงที่ที่ตัดจำหน่าย) คุณควรพิจารณาเช่นกัน
ผลการเปรียบเทียบ
นี่คือการทดสอบที่เกี่ยวข้องกับแผนที่ที่แตกต่าง (มีint
ที่สำคัญและ__int64
/ somestruct
เป็นค่า) std::vector
และ
ข้อมูลประเภททดสอบ:
typeid=__int64 . sizeof=8 . ispod=yes
typeid=struct MediumTypePod . sizeof=184 . ispod=yes
การแทรก
แก้ไข:
ผลลัพธ์ก่อนหน้าของฉันมีจุดบกพร่อง: พวกเขาทดสอบการแทรกตามคำสั่งซึ่งแสดงพฤติกรรมที่รวดเร็วมากสำหรับแผนที่แบบแบน
ฉันทิ้งผลลัพธ์เหล่านั้นไว้ในหน้านี้ในภายหลังเพราะมันน่าสนใจ
นี่คือการทดสอบที่ถูกต้อง:
ฉันได้ตรวจสอบการใช้งานแล้วไม่มีสิ่งที่เรียกว่าการจัดเรียงรอการตัดบัญชีที่ใช้งานในแผนที่แบบแบนที่นี่ การแทรกแต่ละครั้งจัดเรียงอย่างรวดเร็วดังนั้นเกณฑ์มาตรฐานนี้จึงแสดงแนวโน้มที่ไม่แสดงอาการ:
แผนที่:
แฮชแมป O (N * log (N)) :
เวกเตอร์O (N) และแฟลตแมป: O (N * N)
คำเตือน : ปรโลก 2 การทดสอบstd::map
และทั้งสองflat_map
ถูกจัดรถทดสอบจริงและได้รับคำสั่งแทรก (VS แทรกสุ่มสำหรับภาชนะอื่น ๆ ใช่มันสับสนเสียใจ.)
เราจะเห็นว่าการแทรกตามลำดับส่งผลให้เกิดการดันกลับและรวดเร็วมาก อย่างไรก็ตามจากผลลัพธ์ที่ไม่ได้จัดทำแผนภูมิของเกณฑ์มาตรฐานของฉันฉันยังสามารถพูดได้ว่านี่ไม่ได้อยู่ใกล้กับความเหมาะสมที่แท้จริงสำหรับการแทรกกลับ ที่องค์ประกอบ 10k การเพิ่มประสิทธิภาพการแทรกกลับที่สมบูรณ์แบบจะได้รับบนเวกเตอร์ที่จองไว้ล่วงหน้า ซึ่งทำให้เรา 3 ล้านล้านรอบ; เราสังเกต 4.8M ที่นี่สำหรับการแทรกคำสั่งลงในflat_map
(ดังนั้น 160% ของที่ดีที่สุด)
การวิเคราะห์: จำไว้ว่านี่คือ 'การแทรกแบบสุ่ม' สำหรับเวกเตอร์ดังนั้น 1 พันล้านรอบขนาดใหญ่จึงมาจากการต้องเลื่อนข้อมูลครึ่งหนึ่ง (โดยเฉลี่ย) ขึ้นไป (หนึ่งองค์ประกอบต่อหนึ่งองค์ประกอบ) ในการแทรกแต่ละครั้ง
การค้นหา 3 องค์ประกอบแบบสุ่ม (เปลี่ยนนาฬิกาเป็น 1)
ขนาด = 100
ขนาด = 10000
การทำซ้ำ
มากกว่าขนาด 100 (เฉพาะประเภท MediumPod)
มากกว่าขนาด 10,000 (เฉพาะประเภท MediumPod)
เกลือเม็ดสุดท้าย
ในที่สุดฉันก็อยากกลับมาที่ "Benchmarking §3 Pt1" (ตัวจัดสรรระบบ) ในการทดลองล่าสุดฉันกำลังทำเกี่ยวกับประสิทธิภาพของแผนที่แฮชที่อยู่แบบเปิดที่ฉันพัฒนาขึ้นฉันวัดช่องว่างประสิทธิภาพมากกว่า 3000% ระหว่าง Windows 7 และ Windows 8 ในบางstd::unordered_map
กรณีการใช้งาน ( อธิบายที่นี่ )
ซึ่งทำให้ฉันต้องการเตือนผู้อ่านเกี่ยวกับผลลัพธ์ข้างต้น (พวกเขาทำบน Win7): ระยะทางของคุณอาจแตกต่างกันไป
ขอแสดงความนับถืออย่างสูง