(อย่าใช้กฎเหล่านี้โดยไม่คิด ดูประเด็นของ ESR เกี่ยวกับตำแหน่งแคชสำหรับสมาชิกที่คุณใช้ร่วมกัน และในโปรแกรมแบบมัลติเธรด ระวังการแบ่งปันสมาชิกที่เขียนโดยเธรดที่แตกต่างกันในทางที่ผิด โดยทั่วไปคุณไม่ต้องการข้อมูลต่อเธรดใน โครงสร้างเดียวเลยด้วยเหตุผลนี้ เว้นแต่ว่าคุณกำลังทำเพื่อควบคุมการแยกด้วย alignas(128)
ขนาดใหญ่ สิ่งนี้ใช้กับ atomic
และ vars ที่ไม่ใช่อะตอมมิก สิ่งที่สำคัญคือเธรดที่เขียนไปยังบรรทัดแคชไม่ว่าพวกมันจะทำเช่นไรก็ตาม)
หลักทั่วไป: ใหญ่ที่สุดไปเล็กที่สุด alignof()
คุณไม่สามารถทำอะไรได้สมบูรณ์แบบทุกที่ แต่กรณีที่พบบ่อยที่สุดในทุกวันนี้คือการใช้งาน C++ ปกติอย่างสมเหตุสมผลสำหรับ CPU 32 หรือ 64 บิตปกติ ประเภทดั้งเดิมทั้งหมดมีขนาดยกกำลัง 2
ประเภทส่วนใหญ่จะมี alignof(T) = sizeof(T)
หรือ alignof(T)
ต่อยอดที่ความกว้างรีจิสเตอร์ของการนำไปใช้งาน ประเภทที่ใหญ่กว่ามักจะมีความสอดคล้องมากกว่าประเภทที่เล็กกว่า
กฎการบรรจุโครงสร้างใน ABI ส่วนใหญ่จะทำให้สมาชิก struct มีการจัดตำแหน่ง alignof(T)
แบบสัมบูรณ์โดยสัมพันธ์กับจุดเริ่มต้นของ struct และตัว struct เองจะสืบทอด alignof()
ที่ใหญ่ที่สุดจากสมาชิกใดๆ ก็ตาม
ใส่สมาชิกแบบ 64 บิตเสมอก่อน (เช่น double
, long long
และ int64_t
) แน่นอนว่า ISO C++ ไม่ได้แก้ไขประเภทเหล่านี้ที่ 64 บิต / 8 ไบต์ แต่ในทางปฏิบัติกับ CPU ทั้งหมดที่คุณสนใจ บุคคลที่ย้ายโค้ดของคุณไปยัง CPU แปลกใหม่สามารถปรับแต่งเค้าโครงโครงสร้างเพื่อปรับให้เหมาะสมได้หากจำเป็น
ตามด้วยตัวชี้ และจำนวนเต็มความกว้างของตัวชี้: size_t
, intptr_t
และ ptrdiff_t
(ซึ่งอาจเป็น 32 หรือ 64 บิต) สิ่งเหล่านี้ล้วนมีความกว้างเท่ากันในการใช้งาน C ++ สมัยใหม่ตามปกติสำหรับ CPU ที่มีรุ่นหน่วยความจำแบบแบน
พิจารณาใส่รายการลิงค์และตัวชี้ซ้าย/ขวาของต้นไม้ก่อนหากคุณสนใจซีพียู x86 และ Intel การไล่ตัวชี้ผ่านโหนดในแผนผังหรือรายการลิงก์ มีบทลงโทษเมื่อที่อยู่เริ่มต้นของ struct อยู่ในหน้า 4k ที่แตกต่างจากสมาชิกที่คุณกำลังเข้าถึง ทำให้พวกเขาเป็นหลักประกันที่ไม่สามารถเป็นเช่นนั้นได้
จากนั้น long
(ซึ่งบางครั้งจะเป็น 32 บิตแม้ว่าตัวชี้จะเป็น 64 บิตใน LLP64 ABI เช่น Windows x64) แต่รับประกันว่ากว้างอย่างน้อยเท่ากับ int
จากนั้น 32 บิต int32_t
, int
, float
, enum
(แยก int32_t
และ float
นำหน้า int
ก็ได้ หากคุณสนใจระบบ 8/16 บิตที่เป็นไปได้ที่ยังคงแพดประเภทเหล่านั้นเป็น 32 บิต หรือทำงานได้ดีกว่าหากระบบจัดเรียงตามธรรมชาติ ระบบดังกล่าวส่วนใหญ่ไม่มีโหลดที่กว้างกว่า (FPU หรือ SIMD) ดังนั้นจึงต้องจัดการประเภทที่กว้างกว่าเป็นหลายชิ้นแยกกันตลอดเวลา)
ISO C++ อนุญาตให้ int
แคบได้ถึง 16 บิตหรือกว้างโดยพลการ แต่ในทางปฏิบัติมันเป็นประเภท 32 บิตแม้แต่บน CPU 64 บิตก็ตาม นักออกแบบ ABI พบว่าโปรแกรมที่ออกแบบมาเพื่อทำงานกับ int
แบบ 32 บิต จะทำให้หน่วยความจำสิ้นเปลือง (และขนาดแคช) หาก int
กว้างกว่า อย่าตั้งสมมติฐานที่อาจก่อให้เกิดปัญหาเรื่องความถูกต้อง แต่สำหรับประสิทธิภาพแบบพกพา คุณเพียงแค่ต้องถูกต้องในกรณีปกติ
ผู้ที่ปรับแต่งโค้ดของคุณสำหรับแพลตฟอร์มที่แปลกใหม่สามารถปรับแต่งได้หากจำเป็น หากเลย์เอาต์ของโครงสร้างบางอย่างมีความสำคัญอย่างยิ่งยวด คุณอาจแสดงความคิดเห็นเกี่ยวกับสมมติฐานและเหตุผลของคุณในส่วนหัว
แล้วก็ short
/ int16_t
แล้วก็ char
/ int8_t
/ bool
(สำหรับแฟล็ก bool
หลายรายการ โดยเฉพาะอย่างยิ่งหากเป็นแบบอ่านส่วนใหญ่หรือหากแฟล็กทั้งหมดมีการแก้ไขร่วมกัน ให้พิจารณารวมแฟล็กเหล่านั้นด้วยบิตฟิลด์ 1 บิต)
(สำหรับประเภทจำนวนเต็มที่ไม่ได้ลงนาม ให้ค้นหาประเภทการลงนามที่เกี่ยวข้องในรายการของฉัน)
อาร์เรย์ แบบหลายไบต์จาก 8 ไบต์ที่มีประเภทแคบกว่าสามารถไปเร็วกว่านี้ได้หากต้องการ แต่หากคุณไม่ทราบขนาดที่แน่นอนของประเภท คุณไม่สามารถรับประกันได้ว่า int i
+ char buf[4]
จะเติมเต็มช่องที่จัดแนวขนาด 8 ไบต์ระหว่าง double
s สองอัน แต่มันไม่ใช่สมมติฐานที่ไม่ดี ดังนั้นฉันจะทำต่อไปหากมีเหตุผลบางอย่าง (เช่น พื้นที่เชิงพื้นที่ของสมาชิกที่เข้าถึงร่วมกัน) เพื่อรวมพวกเขาเข้าด้วยกันแทนที่จะรวมไว้ตอนท้าย
ประเภทแปลกใหม่: x86-64 System V มี alignof(long double) = 16
แต่ i386 System V มีเพียง alignof(long double) = 4
, sizeof(long double) = 12
เป็นประเภท x87 80 บิต ซึ่งจริงๆ แล้วมีขนาด 10 ไบต์ แต่เสริมเป็น 12 หรือ 16 ดังนั้นจึงเป็นผลคูณของการจัดตำแหน่ง ทำให้อาร์เรย์เป็นไปได้โดยไม่ละเมิดการรับประกันการจัดตำแหน่ง
และโดยทั่วไป จะยุ่งยากมากขึ้นเมื่อสมาชิก struct ของคุณรวมเข้าด้วยกัน (struct หรือ union) ด้วย sizeof(x) != alignof(x)
สิ่งที่บิดเบี้ยวอีกอย่างคือใน ABI บางตัว (เช่น Windows 32 บิตถ้าฉันจำได้ถูกต้อง) สมาชิก struct จะถูกจัดแนวตามขนาด (สูงสุด 8 ไบต์) สัมพันธ์กับจุดเริ่มต้นของ struct แม้ว่า alignof(T)
จะเป็น ยังคงมีเพียง 4 สำหรับ double
และ int64_t
นี่เป็นการปรับให้เหมาะสมสำหรับกรณีทั่วไปของการจัดสรรหน่วยความจำที่จัดแนว 8 ไบต์แยกกันสำหรับโครงสร้างเดียว โดยไม่ต้องให้ การรับประกัน การจัดตำแหน่ง i386 System V ยังมี alignof(T) = 4
เหมือนกันสำหรับประเภทดั้งเดิมส่วนใหญ่ (แต่ malloc
ยังคงให้หน่วยความจำที่จัดแนว 8 ไบต์เพราะ alignof(maxalign_t) = 8
) แต่อย่างไรก็ตาม i386 System V ไม่มีกฎการบรรจุโครงสร้างนั้น ดังนั้น (ถ้าคุณไม่จัดเรียงโครงสร้างของคุณจากใหญ่ที่สุดไปเล็กที่สุด) คุณสามารถจบลงด้วยสมาชิก 8 ไบต์ที่อยู่ต่ำกว่าแนวสัมพันธ์กับจุดเริ่มต้นของโครงสร้าง .
CPU ส่วนใหญ่มีโหมดการกำหนดแอดเดรสที่อนุญาตให้เข้าถึงออฟเซ็ตไบต์ใดๆ เมื่อมีตัวชี้ในรีจิสเตอร์ โดยทั่วไปออฟเซ็ตสูงสุดจะมีขนาดใหญ่มาก แต่ใน x86 จะบันทึกขนาดโค้ดหากออฟเซ็ตไบต์พอดีกับไบต์ที่เซ็นชื่อ ([-128 .. +127]
) ดังนั้นหากคุณมี อาร์เรย์ประเภทใดๆ จำนวนมาก แนะนำให้วางไว้ภายหลังในโครงสร้าง หลังสมาชิกที่ใช้บ่อย แม้ว่าจะต้องเสียค่ารองพื้นสักหน่อยก็ตาม
คอมไพเลอร์ของคุณมักจะสร้างโค้ดที่มีที่อยู่ struct ในรีจิสเตอร์เสมอ ไม่ใช่ที่อยู่ตรงกลางของ struct เพื่อใช้ประโยชน์จากการแทนที่เชิงลบระยะสั้น
Eric S. Raymond เขียนบทความ The Lost Art of Structure Packing โดยเฉพาะส่วนที่เกี่ยวกับการเรียงลำดับโครงสร้างใหม่นั้นเป็นคำตอบสำหรับคำถามนี้
เขายังกล่าวถึงประเด็นสำคัญอีกประการหนึ่ง:
9. ความสามารถในการอ่านและตำแหน่งแคช
แม้ว่าการจัดเรียงใหม่ตามขนาดเป็นวิธีที่ง่ายที่สุดในการกำจัดสิ่งที่เลอะเทอะ นั่นไม่ใช่สิ่งที่ถูกต้องเสมอไป ยังมีอีกสองประเด็น: ความสามารถในการอ่านและตำแหน่งแคช
ในโครงสร้าง ขนาดใหญ่ ที่สามารถแยกข้ามขอบเขตแคชไลน์ได้อย่างง่ายดาย มันสมเหตุสมผลที่จะวาง 2 สิ่งไว้ใกล้กันหากใช้ร่วมกันเสมอ หรือแม้กระทั่งต่อเนื่องกันเพื่อให้สามารถบรรทุก/จัดเก็บรวมกันได้ เช่น คัดลอก 8 หรือ 16 ไบต์ด้วยจำนวนเต็มหนึ่ง (ไม่มีเครื่องหมาย) หรือโหลด/จัดเก็บ SIMD แทนที่จะแยกโหลดสมาชิกที่มีขนาดเล็กกว่า
โดยทั่วไปบรรทัดแคชจะมีขนาด 32 หรือ 64 ไบต์บน CPU สมัยใหม่ (บน x86 สมัยใหม่จะมีขนาด 64 ไบต์เสมอ และตระกูล Sandybridge มีตัวดึงข้อมูลเชิงพื้นที่บรรทัดที่อยู่ติดกันในแคช L2 ที่พยายามทำให้คู่บรรทัดขนาด 128 ไบต์สมบูรณ์ แยกจากตัวตรวจจับรูปแบบการดึงข้อมูลล่วงหน้า HW ลำแสงหลัก L2 และการดึงข้อมูลล่วงหน้า L1d)
เรื่องน่ารู้: Rust ช่วยให้คอมไพเลอร์สามารถจัดลำดับโครงสร้างใหม่เพื่อการบรรจุที่ดีขึ้น หรือเหตุผลอื่น ๆ IDK หากคอมไพเลอร์ใด ๆ ทำเช่นนั้นจริง ๆ อาจเป็นไปได้เฉพาะกับการปรับให้เหมาะสมทั้งโปรแกรมเวลาลิงก์เท่านั้น หากคุณต้องการให้ตัวเลือกขึ้นอยู่กับวิธีการใช้โครงสร้างจริง มิฉะนั้น ส่วนที่คอมไพล์แยกกันของโปรแกรมอาจไม่สอดคล้องกับเค้าโครง
(@alexis โพสต์คำตอบเฉพาะลิงก์ที่ลิงก์ไปยังบทความของ ESR ดังนั้นขอบคุณสำหรับจุดเริ่มต้นนั้น)
person
Peter Cordes
schedule
26.06.2019
struct foo { int a, b; };
) - person David C. Rankin   schedule 26.06.2019double
จะเหมือนกับfloat
(4 ไบต์) ซึ่งอาจเป็นเรื่องแปลกใจสำหรับบางคน (โดยเฉพาะถ้ามีเลขนัยสำคัญมากกว่า 7-8 หลัก) จำเป็นสำหรับตัวนับความถี่...) - person Peter Mortensen   schedule 26.06.2019