การแสวงหาของนักพัฒนาเพื่อเอาชนะฟังก์ชัน memcpy
มาตรฐานได้จุดประกายการอภิปรายทางเทคนิคที่น่าสนใจเกี่ยวกับการปรับปรุงประสิทธิภาพหน่วยความจำและประสิทธิผลของการใช้งานแบบกำหนดเอง การทดลองนี้เกี่ยวข้องกับการสร้างวิธีการคัดลอกหน่วยความจำเฉพาะหลายแบบโดยใช้คำสั่ง CPU ขั้นสูง แต่ผลลัพธ์เผยให้เห็นบทเรียนสำคัญเกี่ยวกับการปรับปรุงประสิทธิภาพ
ข้อมูลจำเพาะของระบบทดสอบ:
- CPU: AMD Ryzen 7 1700X
- RAM: 32GB DDR4 @ 2400MHz
- ช่วงการทดสอบประสิทธิภาพ: การคัดลอกข้อมูล 32MB ถึง 64MB
- เครื่องมือที่ใช้: Google Benchmark
Standard Library ส่งมอบประสิทธิภาพสูงสุดอยู่แล้ว
การอภิปรายในชุมชนเน้นประเด็นสำคัญเกี่ยวกับ standard library สมัยใหม่ แม้จะมีการใช้งานแบบกำหนดเองหลายแบบที่ใช้คำสั่ง SIMD, การทำ vectorization ด้วย AVX และ non-temporal moves แต่ฟังก์ชัน memcpy
มาตรฐานยังคงแข่งขันได้ในกรณีการใช้งานส่วนใหญ่ สิ่งนี้ไม่น่าแปลกใจสำหรับนักพัฒนาที่มีประสบการณ์ที่เข้าใจว่าการใช้งาน standard library ได้รับการปรับปรุงมาหลายทศวรรษและปรับตัวโดยอัตโนมัติกับสถาปัตยกรรมฮาร์ดแวร์ที่แตกต่างกัน
การใช้งาน glibc ใช้เทคนิคที่ซับซ้อนรวมถึง Enhanced Rep Movsb สำหรับการคัดลอกแบบธรรมดาและคำสั่ง AVX สำหรับบล็อกหน่วยความจำที่ไม่ได้จัดตำแหน่ง มันสลับไปมาระหว่างกลยุทธ์ต่างๆ อย่างชาญฉลาดตามขนาดข้อมูลและการจัดตำแหน่ง ทำให้การใช้งานแบบกำหนดเองยากที่จะเอาชนะได้อย่างสม่ำเสมอ
วิธีการ Benchmark ทำให้เกิดคำถาม
สมาชิกชุมชนหลายคนชี้ให้เห็นปัญหาที่อาจเกิดขึ้นกับแนวทางการวัดประสิทธิภาพ ความกังวลหลักคือการวัดเวลาได้คำนึงถึงการจัดเรียงคำสั่ง CPU อย่างเหมาะสมหรือไม่ และข้อมูลที่คัดลอกได้รับการเข้าถึงจริงหลังจากการดำเนินการเสร็จสิ้น CPU สมัยใหม่ดำเนินการคำสั่งแบบไม่เรียงลำดับ ซึ่งสามารถทำให้การดำเนินการคัดลอกหน่วยความจำดูเร็วกว่าที่เป็นจริงหาก benchmark ไม่บังคับให้เสร็จสิ้น
benchmarks เหล่านี้ไม่เกี่ยวข้องเนื่องจาก CPU ดำเนินการคำสั่งแบบไม่เรียงลำดับ ส่วนใหญ่แล้ว cpu จะดำเนินการ assembly ต่อไปในขณะที่การดำเนินการคัดลอกกำลังดำเนินอยู่
ปัญหาอื่นที่ถูกยกขึ้นคือการขาดการควบคุมแคชฮาร์ดแวร์ ซึ่งสามารถส่งผลกระทบอย่างมีนัยสำคัญต่อการวัดประสิทธิภาพหน่วยความจำและทำให้ผลลัพธ์เชื่อถือได้น้อยลง
กรณีเฉพาะแสดงแนวโน้มที่ดี
แม้ว่าข้อสรุปโดยรวมจะเอื้อต่อการใช้งานมาตรฐาน แต่การทดลองได้เผยให้เห็นรูปแบบที่น่าสนใจบางอย่าง สำหรับกรณีการใช้งานที่เฉพาะเจาะจงมากที่มีการจัดตำแหน่งหน่วยความจำที่ควบคุมได้และขนาดข้อมูลขนาดใหญ่ แนวทางแบบกำหนดเองบางอย่างแสดงการปรับปรุง วิธี streaming prefetch มีประสิทธิภาพดีสำหรับการคัดลอกที่ใหญ่กว่า 128MB ในขณะที่ unrolled AVX โดดเด่นในช่วงขนาดเล็กถึงกลาง
อย่างไรก็ตาม ผลกำไรเหล่านี้มาพร้อมกับการแลกเปลี่ยนที่สำคัญ การใช้งานแบบกำหนดเองมักจะมีประสิทธิภาพต่ำนอกเงื่อนไขที่เหมาะสมที่สุดและต้องการข้อกำหนดการจัดตำแหน่งที่เข้มงวดซึ่งไม่เหมาะสมสำหรับการใช้งานทั่วไป
สรุปผลการทดสอบประสิทธิภาพ:
- ดีที่สุดสำหรับการคัดลอกขนาดใหญ่ (128MB+): วิธี Streaming prefetch
- ดีที่สุดสำหรับขนาดเล็ก-กลาง: Unrolled AVX
- เสถียรที่สุด: Standard library memcpy/memmove
- แย่ที่สุดโดยรวม: การใช้งาน Regular memmove
- ประโยชน์จาก Unrolling: ปรับปรุงประสิทธิภาพได้ 5-10% ในกรณีส่วนใหญ่
ความกังวลด้านความปลอดภัยและความถูกต้อง
การอภิปรายยังได้สัมผัสกับข้อพิจารณาด้านความปลอดภัยที่สำคัญ คำสั่ง Non-temporal แม้ว่าจะอาจเร็วกว่า แต่ก็นำเสนอความซับซ้อนในการจัดลำดับที่สามารถนำไปสู่พฤติกรรมที่ไม่ได้กำหนดไว้หากไม่มี memory fence ที่เหมาะสม Standard library จัดการกรณีขอบเขตเหล่านี้โดยอัตโนมัติ ในขณะที่การใช้งานแบบกำหนดเองต้องการความใส่ใจอย่างระมัดระวังต่อการจัดลำดับหน่วยความจำและการจัดการการทับซ้อน
นักพัฒนาได้ตั้งชื่อไฟล์การใช้งานแบบกำหนดเองของตนเองอย่างชาญฉลาดว่า dangerous.cc พร้อมคำเตือนเกี่ยวกับปัญหาที่อาจเกิดขึ้น โดยยอมรับความเสี่ยงที่เกี่ยวข้องในการข้ามฟังก์ชัน standard library ที่ผ่านการทดสอบอย่างดี
วิธีการใช้งาน Custom Implementation ที่ทำการทดสอบ:
- Basic SIMD MOVSD: ลูป assembly แบบ vectorized อย่างง่าย
- Aligned AVX: การดำเนินการ vector 32-byte พร้อมข้อกำหนดการจัดตำแหน่ง
- Stream Aligned AVX: การดำเนินการที่ข้าม cache สำหรับการคัดลอกข้อมูลขนาดใหญ่
- Stream AVX with Prefetch: การ prefetch ข้อมูลใน cache สำหรับข้อมูลในการทำซ้ำครั้งถัดไป
- เวอร์ชัน Unrolled: การคลาย loop ด้วยตัวคูณ 4x เพื่อลดการแยกสาขา
คำตัดสินเกี่ยวกับการปรับปรุงประสิทธิภาพหน่วยความจำ
การทดลองนี้เป็นเครื่องเตือนใจที่มีค่าว่าการปรับปรุงประสิทธิภาพไม่ได้เกี่ยวกับการเขียนโค้ดแบบกำหนดเองเสมอไป Standard library สมัยใหม่แสดงถึงงานปรับปรุงหลายทศวรรษโดยผู้เชี่ยวชาญที่เข้าใจทั้งความสามารถของฮาร์ดแวร์และข้อกำหนดที่ละเอียดอ่อนของการจัดการหน่วยความจำที่ถูกต้อง แม้ว่าการสำรวจการใช้งานแบบกำหนดเองอาจเป็นการศึกษา แต่คำแนะนำเชิงปฏิบัติยังคงชัดเจน: ยึดติดกับฟังก์ชัน standard library เว้นแต่คุณจะมีข้อกำหนดที่เฉพาะเจาะจงมากและความเชี่ยวชาญในการจัดการความเสี่ยงที่เกี่ยวข้อง
SIMD: Single Instruction, Multiple Data - ประเภทของการประมวลผลแบบขนานที่ดำเนินการเดียวกันกับจุดข้อมูลหลายจุดพร้อมกัน
AVX: Advanced Vector Extensions - ชุดคำสั่ง CPU ที่เปิดใช้งานการดำเนินการแบบ vectorized บนชิ้นข้อมูลที่ใหญ่กว่า
คำสั่ง Non-temporal: คำสั่ง CPU ที่ข้ามหน่วยความจำแคชสำหรับการดำเนินการบางอย่าง
อ้างอิง: Going faster than memcpy