1 คะแนน โดย GN⁺ 2024-01-06 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

เพิ่มความเร็วของโค้ด: อย่าส่งผ่าน struct ที่ใหญ่กว่า 16 ไบต์บน AMD64

  • เพื่อปรับปรุงประสิทธิภาพของภาษา Neat ได้เปลี่ยนวิธีส่งอาร์เรย์จากการใช้พารามิเตอร์ struct หนึ่งตัวเป็นการส่งพารามิเตอร์ pointer สามตัวแทน
  • สาเหตุที่อาร์เรย์ของ Neat ช้ากว่าอาร์เรย์ของภาษา D คืออาร์เรย์ขนาด 24 ไบต์มีขนาดเกิน 16 ไบต์ จึงถูกส่งพารามิเตอร์ด้วยวิธีที่ต่างออกไป
  • ตามข้อกำหนด SystemV AMD64 ABI, struct ทุกตัวที่มีขนาดเกิน 16 ไบต์จะถูกส่งผ่าน pointer

ตรวจสอบปัญหาด้วยเบนช์มาร์ก

  • ใช้เบนช์มาร์กเพื่อยืนยันความแตกต่างด้านประสิทธิภาพระหว่างการส่ง struct ทั้งก้อนกับการส่งฟิลด์แยกกัน
  • เมื่อส่ง struct จำเป็นต้องมีขั้นตอนจัดสรรบนสแตกและคัดลอกข้อมูล แต่เมื่อส่งฟิลด์แยกกันสามารถส่งผ่าน SSE register ได้โดยตรง
  • วิธีส่งฟิลด์แยกกันให้ประสิทธิภาพเร็วกว่าวิธีส่ง struct ประมาณ 2 เท่า

ทางเลือกของผู้ออกแบบภาษา

  • เมื่อต้องเรียก C API จำเป็นต้องปฏิบัติตาม C ABI แต่ชนิดข้อมูลระดับสูงที่ใช้ภายในไม่จำเป็นต้องแทนด้วย struct เสมอไป
  • ผู้ออกแบบภาษาสามารถตัดสินใจได้ว่าอาร์เรย์ ทูเพิล และ sum type จะถูกส่งผ่านอย่างไร
  • การส่งชนิดข้อมูลที่มีขนาดเกิน 16 ไบต์แบบแยกฟิลด์อาจช่วยเพิ่มประสิทธิภาพได้

ความเห็นของ GN⁺

  • บทความนี้มีประโยชน์อย่างมากสำหรับนักพัฒนาที่สนใจการเพิ่มประสิทธิภาพซอฟต์แวร์
  • โดยเฉพาะอย่างยิ่ง มันแสดงให้เห็นว่าเมื่อพัฒนาแอปพลิเคชันที่ไวต่อประสิทธิภาพ ขนาดของ struct และวิธีการส่งผ่านสามารถส่งผลสำคัญได้
  • ผู้ออกแบบภาษาและนักพัฒนา API สามารถใช้ข้อมูลนี้เพื่อหาโอกาสในการปรับปรุงประสิทธิภาพได้

1 ความคิดเห็น

 
GN⁺ 2024-01-06
ความคิดเห็นบน Hacker News
  • สำหรับประเด็นเรื่อง SysV amd64 ABI นั้น สามารถตั้งค่า ABI ภายในภาษาของตัวเองให้ไม่ใช่ SysV ก็ได้ ตราบใดที่ไม่ได้เปิดเผยออกไปยังตัวเรียก C แบบ SysV ก็สามารถใช้ calling convention แบบใดก็ได้ที่ต้องการ ความแตกต่างของ NeatLang ดูซับซ้อนกว่าการเปลี่ยน LLVM calling convention มาก และผู้เขียนอาจต้องการเปิดเผย type ให้กับโปรแกรม C ด้วย calling convention ที่คงที่
  • มักมีความเข้าใจไม่เพียงพอเกี่ยวกับต้นทุนของการส่งอาร์กิวเมนต์ และบทความที่เขียนเกี่ยวกับเรื่องนี้มีประโยชน์ ตัวอย่างเช่น ที่ Google มีธรรมเนียมส่งอ็อบเจ็กต์ขนาด 24 ไบต์แบบส่งค่า ซึ่งไม่ปรากฏใน profiler แต่มีต้นทุนเกิดขึ้นในทุกฟังก์ชัน
  • ตอนย้ายไป x64 ได้ทำ benchmark เอนจินกราฟิกเพราะกังวลว่าอ็อบเจ็กต์ vec3 (3xfloat) จะขยายจาก 12 ไบต์เป็น 16 ไบต์ พบว่าการใช้ 16 ไบต์กลับเร็วกว่าเพราะสอดคล้องกับการอ่าน 8 ไบต์ สุดท้ายจึงใช้ vec3 เหมือน vec4 แนะนำให้ทำ benchmark แบบองค์รวมเสมอ
  • อาร์กิวเมนต์ที่ preload อยู่ในรีจิสเตอร์มีประสิทธิภาพดีกว่าการเขียนลงสแตก และการจัดการสแตกก็เร็วกว่าของที่จัดสรรบนฮีป นี่เป็นเหตุผลว่าทำไมโค้ดซับซ้อนที่มีตัวแปรโกลบอลจำนวนมากจึงรันได้เร็ว และฟังก์ชันเวียนเกิดที่สวยงามหรืออาร์กิวเมนต์แบบ tuple/struct/list จึงช้า แบบแรกนั้นปรับแต่งให้เป็นลูปแอสเซมบลีที่หนาแน่นได้ง่ายกว่า
  • ใน MSVC โครงสร้างที่เกิน 8 ไบต์จะถูกส่งผ่านสแตก นี่เป็นรายละเอียดของ ABI ที่ไม่ควรนำไปพึ่งพาในโค้ดที่ต้องพกพาได้ อย่างไรก็ตาม สำหรับฟังก์ชันที่ไม่ได้ถูกเรียกบ่อยก็ไม่ต้องเครียดมาก และสำหรับฟังก์ชันเล็ก ๆ ที่ถูกเรียกบ่อย ควรเปิดโอกาสให้คอมไพเลอร์ inline โค้ดได้ เพื่อเปิดใช้การปรับแต่งที่มีประโยชน์มากกว่าการส่งอาร์กิวเมนต์ผ่านรีจิสเตอร์
  • บน Windows เมื่อใช้ calling convention แบบ cdecl ตามค่าเริ่มต้น โครงสร้างที่ใหญ่กว่า 8 ไบต์จะไม่ถูกส่งผ่านรีจิสเตอร์
  • บน amd64 การส่งและคืนค่าโครงสร้างที่ใหญ่กว่า 16 ไบต์แบบส่งค่าโดยใช้ sysv amd64 ABI นั้นช้า แต่ก็มักคุ้มค่าเพื่อให้โค้ดชัดเจนขึ้น แน่นอนว่าในกรณีนี้ไม่เกี่ยวข้อง แต่ยกตัวอย่างเช่น C++ compiler แต่ละตัว, Golang, OCaml และ SBCL สามารถใช้ ABI ที่กำหนดเองภายในภาษาของตัวเองได้
  • ใน C++ มีกฎคร่าว ๆ ว่าประเภทที่ไม่ใช่ primitive ควรถูกส่งผ่าน reference (หรือถ้าจำเป็นก็ pointer) เว้นแต่จะมีเหตุผลที่ดีจริง ๆ ส่วนหนึ่งก็เพราะ ABI และเพื่อหลีกเลี่ยง copy constructor หรือ move constructor นี่เป็นรายละเอียดระดับล่างที่น่าเบื่อแต่ต้องใส่ใจหากต้องการปรับจูนประสิทธิภาพใน C++
  • บทความให้ลิงก์ไปยัง benchmark ที่เฉพาะทางมาก ซึ่งในนั้น Java (JIT) เร็วกว่า C++ และเร็วกว่า Scala เสียอีก ทำให้เกิดคำถามว่า Julia HO คืออะไรและทำไมถึงเร็วขนาดนั้น เหตุใดความต่างความเร็วระหว่าง Python กับ Pypy จึงมาก และมีเหตุผลอะไรบ้างที่จะไม่ใช้ Pypy หรือควรกลายเป็นมาตรฐานหรือไม่
  • ในตัวอย่างที่ให้มา สามารถแก้ได้โดยเปลี่ยนชนิดพารามิเตอร์ struct Vector ให้ส่งเป็น reference แบบ const struct Vector & โดยไม่กระทบต่อผู้เรียก โค้ด C++ จำนวนมากที่มีบั๊กจาก pointer ใช้ pointer โดยไม่จำเป็น ทั้งที่การส่งผ่าน reference ใช้งานได้ง่ายและปลอดภัยกว่า