- SIMD ไม่ใช่เทคนิคซับซ้อนสำหรับซอฟต์แวร์สมรรถนะสูงเท่านั้น แต่เป็นวิธีเพิ่มประสิทธิภาพที่ใช้ได้ในชีวิตประจำวัน โดยประมวลผลข้อมูลต่อเนื่องครั้งละหลายค่าเพื่อเร่งลูปธรรมดา
- โดยทั่วไปโค้ด SIMD จะมีโครงสร้าง 5 ขั้นตอนคือ กระจายค่าคงที่, วนลูปทีละเวกเตอร์, คำนวณแบบขนาน, ยุบผลลัพธ์หรือบันทึกผล, และจัดการส่วนท้ายแบบสเกลาร์
- ลูปค้นหา code point ของ Ghostty เปรียบเทียบ
u32 ได้ครั้งละ 4·8·16 ค่า และในทางทฤษฎีสามารถเพิ่ม throughput ได้สูงสุด 4 เท่าบน ARM NEON, 8 เท่าบน AVX2 และ 16 เท่าบน AVX-512
- throughput โดยรวมของเทอร์มินัลบนเดสก์ท็อป Intel ที่รองรับ AVX2 เร็วขึ้นประมาณ 5 เท่า และถ้าไม่มีความกว้างเวกเตอร์ที่รองรับหรือยังมีอินพุตเหลืออยู่ ลูปสเกลาร์เดิมจะจัดการทั้งอินพุตหรือส่วนที่เหลือ
- auto-vectorization ของคอมไพเลอร์อาจพลาดโอกาสแม้ในลูปง่าย ๆ ดังนั้นควรตรวจผลลัพธ์ที่ optimize แล้วก่อน แต่สำหรับ hot loop สำคัญ การเขียน SIMD แบบชัดเจนช่วยให้พฤติกรรมและประสิทธิภาพคาดเดาได้
SIMD ทำอะไร
- SIMD ทำให้ CPU ประมวลผลหลายค่าแบบขนานด้วยคำสั่งเดียว
- แทนที่จะเปรียบเทียบทีละไบต์ ก็สามารถเปรียบเทียบได้ครั้งละ 4, 8 หรือมากกว่านั้น
- ลูปอย่าง
for (byte in bytes), for (character in string), for (value in array) มีโอกาสแปลงเป็นการประมวลผล ตามความกว้างเวกเตอร์
- ถ้าข้อมูลมีตั้งแต่หลายร้อย หลายพัน หรือหลายล้านไบต์ ก็อาจได้ความเร็วเพิ่มเฉพาะจุด 4 เท่า, 8 เท่าหรือมากกว่านั้นตามความกว้างการประมวลผลแบบขนาน
- ถ้าข้อมูลมีแค่ไม่กี่ค่าหรือหลักสิบ การใช้ SIMD ก็ไม่คุ้ม
- simdutf และ simdjson ใช้เทคนิค SIMD ที่ซับซ้อน แต่ SIMD ในงานทั่วไปไม่จำเป็นต้องซับซ้อนขนาดนั้น
- ตัวอย่างใช้ Zig แต่ โครงสร้าง 5 ขั้นตอน นี้ใช้ได้กับภาษาอื่นด้วย เพียงแต่แต่ละภาษารองรับคำสั่ง SIMD ต่างกัน
โครงสร้าง 5 ขั้นตอนที่เกิดซ้ำ
- กระจายค่าคงที่ที่ต้องใช้ไปยังทุก lane และถ้าจำเป็นก็เริ่มต้น vector accumulator
- วนอินพุตครั้งละ ขนาดความกว้างเวกเตอร์
- รันการเปรียบเทียบหรือคำนวณเลขคณิตแบบขนานบนทุก lane
- ยุบผลลัพธ์ของเวกเตอร์หรือบันทึกผลตามอัลกอริทึม
- ส่วนที่เหลือซึ่งใส่เวกเตอร์เต็มไม่ได้ให้จัดการด้วย scalar tail ซึ่งก็คือลูปเดิม
- เมื่อคุ้นกับโครงสร้างนี้แล้ว คุณจะสามารถแยกลูปทั่วไปออกเป็น 5 ขั้นตอนเดียวกัน ทำให้การเขียน SIMD ง่ายพอ ๆ กับลูปสเกลาร์
- ถ้ายังอธิบายด้วยโครงสร้างนี้อย่างง่ายไม่ได้ ตอนนี้อาจเหมาะกว่าที่จะข้ามการใช้ SIMD ไปก่อน
ลูปค้นหาจริงของ Ghostty
- Ghostty จะกินข้อมูลจากอาร์เรย์ code point ที่ถอดรหัสแล้วจนกว่าจะเจอค่าที่น้อยกว่าหรือเท่ากับ
0xF
- ข้อมูลของเทอร์มินัลส่วนใหญ่เป็นตัวอักษรธรรมดาที่ต้องแสดงผล จึงสามารถจับเป็นชุดเพื่อประมวลผลได้
- ลูปนี้พยายามหาจุดสิ้นสุดของช่วงที่ยังพิมพ์ออกได้ถัดไปให้เร็วที่สุด
- เดิมที implementation แบบสเกลาร์จะตรวจ code point ทีละตัว
while (end < cps.len and cps[end] > 0xF) end += 1;
- implementation แบบเวกเตอร์ใช้เวกเตอร์ทั่วไปโดยไม่พึ่ง builtin เฉพาะ CPU และมีโค้ดเพิ่มจากแบบสเกลาร์เพียง 12 บรรทัด
- การเพิ่ม throughput ที่คาดหวังสอดคล้องกับจำนวน lane ของเวกเตอร์
- ARM NEON และ Apple Silicon: สูงสุด 4 เท่า
- AVX2 ที่ CPU x86 สมัยใหม่ส่วนใหญ่รองรับ: สูงสุด 8 เท่า
- AVX-512 ที่รองรับบน Intel บางรุ่นและ AMD Zen 4 ขึ้นไป: สูงสุด 16 เท่า
- บนเดสก์ท็อป Intel ที่รองรับ AVX2 throughput โดยรวมที่วัดตั้งแต่อินพุตของโปรแกรมเทอร์มินัลไปจนถึงสถานะสุดท้ายของเทอร์มินัลเร็วขึ้นประมาณ 5 เท่า
- ไม่ได้ความเร็วตามทฤษฎีเต็มทั้งหมด เพราะยังมีงานรอบ ๆ SIMD ด้วย
- อักขระควบคุม C0 ยังมีอยู่หลัง
0xF แต่ 0xF คือเกณฑ์ที่ใช้ในเส้นทางโค้ดของ Ghostty นี้
- ESC และ control sequence อื่น ๆ จะถูกจัดการในอีกเส้นทางหนึ่ง
ขั้นตอนที่ 1: กระจายค่าคงที่
if (simd.lanes(u32)) |lanes| {
const V = @Vector(lanes, u32);
const threshold: V = @splat(0xF);
simd.lanes(u32) ของ Ghostty จะคืนจำนวน u32 ที่ CPU เป้าหมายประมวลผลพร้อมกันได้
- แต่ละค่าถูกเรียกว่า lane
- ARM คืน 4, AVX2 คืน 8 และ AVX-512 คืน 16
- ถ้าไม่มีขนาดเวกเตอร์ให้ใช้ จะคืน
null และข้ามโค้ด SIMD ไป
@Vector(lanes, u32) จะสร้างชนิดเวกเตอร์ที่มีจำนวน lane ตามนั้น
- ถ้า
lanes เป็น 8 ก็หมายความว่า V หนึ่งตัวเก็บ u32 สำหรับประมวลผลแบบขนานได้ 8 ค่า
- การเปรียบเทียบเวกเตอร์ต้องให้ทั้งสองฝั่งเป็นเวกเตอร์ จึงใช้
@splat(0xF) เพื่อทำซ้ำค่า 0xF ไปยังทุก lane
{ 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF }
- อัลกอริทึมนี้ไม่ต้องมี vector accumulator แต่บางอัลกอริทึมอาจ initialize accumulator ในขั้นตอนนี้
ขั้นตอนที่ 2: วนทีละหนึ่งเวกเตอร์
while (end + lanes <= cps.len) : (end += lanes) {
const values: V = cps[end..][0..lanes].*;
- ถ้า
lanes เป็น 8 ลูปจะเข้าก็ต่อเมื่อยังเหลือค่าอย่างน้อย 8 ค่า และจะโหลด 8 ค่านั้นเข้า values
- เมื่อจบแต่ละรอบ
end จะเพิ่มทีละ จำนวน lane ไม่ใช่ 1
- ต้องสามารถโหลดเวกเตอร์เต็มได้ ดังนั้นถ้าเหลือเพียง 5 ค่า ก็จะไม่อ่านเวกเตอร์ 8 lane
- ค่าที่ใส่เวกเตอร์ไม่ได้จะไปให้ scalar tail ในขั้นตอนที่ 5 จัดการ
ขั้นตอนที่ 3: เปรียบเทียบแบบขนานบนทุก lane
const greater_than_threshold = values > threshold;
- เมื่อทั้ง
values และ threshold เป็นเวกเตอร์ ตัวดำเนินการ > จะเปรียบเทียบ lane ที่ตรงกันทั้งหมดด้วย vector operation เดียว
- ถ้าเป็น 8 lane ก็เท่ากับทำการเปรียบเทียบ 8 ครั้งแบบขนานสำหรับ
cps[end] > 0xF
values: { 0x41, 0x42, 0x43, 0x0A, 0x44, 0x45, 0x46, 0x47 }
threshold: { 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF }
greater_than_threshold: { true, true, true, false, true, true, true, true }
- ไม่มีลูปด้านในแบบชัดเจน และผลลัพธ์คือเวกเตอร์ที่เก็บค่า boolean แยกตาม lane
- โครงสร้างเดียวกันนี้ใช้ได้ไม่ใช่แค่กับการเปรียบเทียบ แต่รวมถึงการบวก คูณ ค่าต่ำสุด ค่าสูงสุด และการดำเนินการอื่นที่ชนิดเวกเตอร์รองรับ
- แม้การเปรียบเทียบจะเป็น vector operation เดียว แต่การโหลดเวกเตอร์ การยุบผลลัพธ์ และการหาตำแหน่ง lane ที่ล้มเหลวยังต้องใช้คำสั่งเพิ่ม
ขั้นตอนที่ 4: ยุบผลลัพธ์เวกเตอร์
if (@reduce(.And, greater_than_threshold)) continue;
@reduce(.And, ...) จะรวม boolean ทั้งหมดด้วย and ให้เหลือ boolean เดียว
- ถ้าทุก lane เป็น
true ก็ข้ามไปเวกเตอร์ถัดไป แต่ถ้ามีอย่างน้อยหนึ่ง lane เป็น false ก็ต้องหาตำแหน่งที่ล้มเหลวให้เจอ
const mask: std.meta.Int(.unsigned, lanes) = @bitCast(greater_than_threshold);
end += @ctz(~mask);
break;
@bitCast จะแปลงเวกเตอร์ boolean เป็น integer mask ที่มี 1 บิตต่อ lane
1 หมายถึงค่ามากกว่า 0xF
0 หมายถึงการเปรียบเทียบล้มเหลว
- เมื่อกลับบิตของ mask แล้ว การเปรียบเทียบที่ล้มเหลวจะกลายเป็น
1 และ @ctz จะนับจำนวนบิต 0 ก่อนหน้า 1 ตัวแรก
values: { 0x41, 0x42, 0x43, 0x0A, 0x44, 0x45, 0x46, 0x47 }
greater_than_threshold: { true, true, true, false, true, true, true, true }
mask: { 1, 1, 1, 0, 1, 1, 1, 1 }
~mask: { 0, 0, 0, 1, 0, 0, 0, 0 }
- ในตัวอย่างนี้
@ctz(~mask) จะคืน 3 และเลื่อน end ไปยัง lane ลำดับที่ 3 ซึ่งเป็นตำแหน่งของอักขระควบคุมตัวแรก 0x0A
- การยุบผลลัพธ์เป็นส่วนที่ต่างกันมากที่สุดของ 5 ขั้นตอนในแต่ละอัลกอริทึม
- การหาผลรวมอาจยุบ vector accumulator ให้เหลือเลขเดียว
- การแปลงข้อมูลอาจบันทึกทั้งเวกเตอร์ลง output buffer ได้เลย
- การค้นหานี้สร้าง bit mask เพื่อหาตำแหน่งของ lane ที่ต้องการ
ขั้นตอนที่ 5: จัดการ scalar tail
while (end < cps.len and cps[end] > 0xF) end += 1;
- ถ้าความยาวอินพุตไม่หารลงตัวด้วยความกว้างเวกเตอร์ ลูปสเกลาร์เดิมจะจัดการส่วนที่เหลือ
- หลังลูปเวกเตอร์ 8 lane อาจเหลือค่าได้ตั้งแต่ 0 ถึง 7 ค่า
- บน CPU ที่
simd.lanes(u32) คืน null ส่วน SIMD จะถูกข้ามไปและลูปสเกลาร์จะจัดการอินพุตทั้งหมด
- implementation เดิมจึงรับหน้าที่ทั้ง จัดการส่วนที่เหลือและเป็น fallback เพื่อความเข้ากันได้ พร้อมกัน
- เวกเตอร์ทั่วไปช่วยตัดไวยากรณ์เฉพาะ CPU ออก แต่ไม่ได้ตัดการสร้างโค้ดเฉพาะ CPU ทิ้งไป
- Zig จะแปลง vector operation ให้เป็นชุดคำสั่งที่เปิดใช้อยู่บน target นั้น
สิ่งที่ auto-vectorization พลาดได้
- คอมไพเลอร์สามารถทำ auto-vectorization ให้โค้ดง่าย ๆ เช่นลูปคำนวณเลขคณิตที่มีรูปแบบสม่ำเสมอและไม่มี control flow ซับซ้อนได้
- ก่อนจะเขียน SIMD เอง ควรคอมไพล์เวอร์ชันสเกลาร์ด้วยตัวเลือก optimize และตรวจดู โค้ดที่สร้างขึ้น ก่อน
- คอมไพเลอร์สำหรับ production มักพลาดโอกาสในการ vectorize อยู่บ่อย และแม้งานวิจัยด้าน auto-vectorization จะดำเนินมาหลายสิบปี แต่งาน วิจัยล่าสุด ก็ยังเริ่มต้นจากปัญหานี้
- ถ้าลูปนั้นสำคัญพอที่ความเร็วเพิ่ม 5 เท่ามีผล ก็ควรเขียน vectorization แบบชัดเจนเพื่อให้พฤติกรรมคาดเดาได้
- จะช่วยหลีกเลี่ยงสถานการณ์ที่การแก้โค้ดส่วนอื่นหรือการอัปเดตคอมไพเลอร์ทำให้ลูปเวกเตอร์เงียบ ๆ กลับไปเป็นลูปสเกลาร์
ขอบเขต SIMD ที่นักพัฒนาควรเรียนรู้
- เมื่อพบ hot loop ที่ค้นหา เปรียบเทียบ นับ หรือแปลงข้อมูลต่อเนื่องจำนวนมาก คุณควรนึกถึงการประมวลผลตามความกว้างเวกเตอร์ได้
- SIMD ในงานทั่วไปมีรูปแบบที่เป็นระเบียบ ได้แก่ เตรียมค่าคงที่ โหลดเวกเตอร์ คำนวณแบบขนาน ยุบผลลัพธ์ และ scalar tail
- ถ้าภาษารองรับ SIMD ได้ดี คุณก็ปรับปรุงประสิทธิภาพได้โดยไม่ต้องรู้แอสเซมบลีหรือรายละเอียดเฉพาะ CPU ด้วยตัวเอง
- ระดับความรู้ที่นักพัฒนาทุกคนต้องมีไม่ใช่เทคนิคซับซ้อนแบบ
simdutf หรือ simdjson แต่เป็นการมองเห็นโอกาสในการใช้ SIMD และใช้โครงสร้างร่วมเหล่านี้ให้เป็น
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เป็นบทความที่ดี แต่การเริ่มต้นด้วยการบอกว่า SIMD เข้าใจง่ายและเขียนง่ายพอ ๆ กับ for loop แล้วตัวอย่างแรกก็เปลี่ยนโค้ดสเกลาร์หนึ่งบรรทัดให้กลายเป็น 12 บรรทัดนั้นไม่น่าเชื่อถือเท่าไร
พูดตรง ๆ ว่า SIMD นั้นยากแต่ผลลัพธ์คุ้มค่าน่าจะดีกว่า ถ้าเขียนสำหรับมือใหม่ ก็ไม่ควรใช้ศัพท์เฉพาะของ SIMD อย่าง
broadcastตั้งแต่ขั้นแรกโดยไม่อธิบาย ส่วนขั้นที่ 5 ที่อธิบาย การจัดการส่วนท้ายแบบสเกลาร์ นั้นเป็นโครงสร้างที่ดีต้องดูว่าฮาร์ดแวร์ประมวลผลได้ทีละกี่รายการ จัดกลุ่มงานตามขนาดนั้น คลี่ผลลัพธ์กลับมา จัดการรายการที่เหลือแยกต่างหาก และแม้แต่ค่าคงที่ก็ต้องสร้างเป็นเวกเตอร์ที่ทำซ้ำค่าไว้ แต่ละอย่างไม่ได้ยาก แต่เพิ่มปริมาณงานจนทำให้ยุ่งยาก
ฟีเจอร์ที่ชอบที่สุดคือ
par(; ; )ซึ่งคอมไพเลอร์จะทำให้ for loop เป็นแบบขนานโดยอัตโนมัติภายใต้เงื่อนไขขอบเขตบางอย่างถ้าหัวข้อนั้นซับซ้อนจริง ก็ควรแบ่งเป็นส่วนเล็กและเรียบง่ายกว่าเดิม จากนั้นจัดลำดับให้ดีเพื่อช่วยให้ไต่เส้นโค้งการเรียนรู้ที่ชันได้ และทำให้ผู้เรียนเชื่อว่าคุ้มค่า
คำแนะนำที่ดีกว่าคือทุกคนควรรู้จัก array programming เพราะการปรับแต่ง SIMD โดยมากต้องใช้วิธีคิดแบบนั้น และเทคนิคที่เฉพาะกับ packed SIMD เท่านั้นกลับมีน้อยกว่าที่คิด
array programming ทำให้คอมไพเลอร์ทำ auto-vectorization ได้ง่าย จึงมักสร้างโค้ดที่มีประสิทธิภาพดีได้แม้ไม่เขียน SIMD โดยตรง
แม้ประสบการณ์จะยังไม่มาก แต่ Julia ดูเหมือนจะใกล้เคียงที่สุดกับภาษาที่ทันสมัยกว่าและแสดงออกได้ดีกว่า พร้อมความสามารถด้านเวกเตอร์คล้ายกัน
ช่วงไม่กี่วันที่ผ่านมาได้ปรับแต่งการคำนวณเมทริกซ์ในโปรเจกต์ชีวสารสนเทศด้วย AVX-512 และพอใจมาก
แอปพลิเคชันส่วนใหญ่มีคอขวดอยู่ที่การอ่านชุดข้อมูลขนาดใหญ่จากหน่วยความจำ ดังนั้นแทนที่จะอ่านซ้ำ ๆ เพื่อทำหลายการดำเนินการ เราสามารถใช้รีจิสเตอร์ AVX และ fused kernel ประมวลผลทั้งหมดในครั้งเดียวได้ การเร่งความเร็ว 5 เท่าก็พบได้บ่อย และแม้จะใช้อินทรินสิกโดยตรง แต่ถ้าใช้ crate
wideการดำเนินการทั่วไปจะง่ายมาก: https://docs.rs/wide/latest/wide/นักพัฒนาส่วนใหญ่อย่างท่วมท้นไม่มีความจำเป็นต้องเรียน SIMD เลย สงสัยว่าทำไมต้องทำให้คนเข้าใจผิดว่าถ้านักพัฒนาทุกคนไม่รู้สิ่งนี้ก็ไม่ใช่นักพัฒนาจริง ๆ
ควรเปลี่ยนชื่อเป็น “ทุกคนควรรู้ว่า เมื่อไร SIMD ไม่ถูกนำมาใช้” มากกว่า
คอมไพเลอร์ยุคใหม่ทำ vectorization ได้ดีมาก แต่บางครั้งแค่สมมติฐานหนึ่งข้อหรือ branch ที่ขึ้นกับข้อมูลหนึ่งจุด ก็ทำให้ถอยกลับไปเป็นโค้ดสเกลาร์ทันที การรู้วิธีดู รายงานการปรับแต่ง ของคอมไพเลอร์อาจมีค่ามากกว่าการรู้วิธีเขียน SIMD
ถ้าทำได้แค่ระบุปัญหา สุดท้ายก็จบที่ “น่าเสียดาย”
ปีที่แล้วตอนทำออดิโอซินธิไซเซอร์ ผมเริ่มเรียน x86 และ ARM SIMD: https://github.com/seclorum/SIMDSynth
โครงสร้างซินธิไซเซอร์แบบหลาย timbre และหลายเสียงเหมาะมากสำหรับเรียนรู้หลักการ SIMD เพราะนำการประมวลผลเดียวกันไปใช้กับหลายกระแสข้อมูล อย่างไรก็ตาม การดีบักค่อนข้างยากมาก จนรู้สึกว่าต้องการซิมูเลเตอร์ที่ช่วยให้เข้าใจสถานะของแต่ละ processing pipeline ได้จริง ๆ และการสำรวจเครื่องมือ SIMD ก็น่าจะต้องลงทุนลงแรงอีกมาก
บทความดี และอยากให้ภาษามากขึ้นรองรับ SIMD แต่ในสถานการณ์ที่สองภาษายอดนิยมที่สุด ไม่รองรับ SIMD แบบเนทีฟ การพูดว่า “โปรแกรมเมอร์ทุกคนควรรู้” ก็ดูแปลกอยู่บ้าง
แม้คุณจะไม่ได้เขียน SIMD เองหรือวางแผนให้ AI ทำแทน ก็ควรรู้ว่างานแบบใดบนฮาร์ดแวร์แบบใดที่ SIMD ช่วยให้เร็วขึ้นได้ เพื่อจะได้ออกแบบ อัลกอริทึมและโครงสร้างโค้ด ให้เอื้อต่อการใช้ SIMD
ผลกระทบของ data dependency, ต้นทุนของการเพิ่มความกว้างขององค์ประกอบเวกเตอร์และวิธีหลีกเลี่ยง, วิธีเปลี่ยนเงื่อนไขและ branch ให้เป็น mask, คุณสมบัติอย่าง “ไม่มีคำสั่งหาร” จะซึมซับได้ง่ายขึ้นมากหากลองใช้ SIMD โดยตรงแม้เพียงเล็กน้อย
มันทำงานได้ดีเมื่อสแกนหรือแปลงข้อมูลต่อเนื่องขนาดใหญ่ในครั้งเดียว แต่ถ้าต้องตัดสินใจทุก ๆ ไม่กี่ไบต์ของอินพุต ก็อาจเร็วพอ ๆ กับแบบสเกลาร์หรือช้ากว่าได้ SIMD ไม่ใช่ปุ่มเร่งความเร็ววิเศษ
นี่เป็นวิดีโอมีประโยชน์ที่ Casey Muratori อธิบายกระบวนการที่ทีมพัฒนา The Witness ใช้ SIMD แก้ปัญหาประสิทธิภาพจริง: https://www.youtube.com/watch?v=Ge3aKEmZcqY
เป็นตัวอย่างที่ดีของ vertical integration เพื่อประสิทธิภาพ แสดงให้เห็นว่าหลังจากเข้าใจว่าทำไม abstraction ทั่วไปจึงมีอยู่และทำไมต้องเป็นแบบทั่วไปแล้ว ใน use case เฉพาะหนึ่ง ๆ เราสามารถบูรณาการแนวตั้งตั้งแต่นิยามปัญหาไปจนถึง SIMD เพื่อให้ได้ประโยชน์อย่างมาก
ก่อนจะลงไปถึงการปรับแต่งระดับจุลภาคอย่าง SIMD ควรพิจารณา โครงสร้างข้อมูลและรูปแบบการเข้าถึง อย่างจริงจังก่อน
เคยนำ SIMD ไปใช้กับโค้ด Zig เก่า แต่โมเดลโครงสร้างข้อมูลสวนทางกับการปรับให้เหมาะสมโดยสิ้นเชิง เหมือนเอายางรถแข่งสมรรถนะสูงไปใส่รถเก่าที่เครื่องยนต์พัง เป็นการรีบ optimize แบบฉบับที่ไม่ได้วัดประสิทธิภาพ และไม่ได้คำนึงถึงตำแหน่งการจัดสรรหน่วยความจำ
ตอนนี้มองข้อมูลเหมือนตาราง SQL และออกแบบโครงสร้างโดยยึดคีย์หลักที่เป็นไปได้กับรูปแบบการเข้าถึงเป็นศูนย์กลาง เมื่อก่อนใช้ tree ที่ชี้ไปยัง struct อื่น ๆ บน heap จึงรับข้อเสียทั้งหมด ทั้งข้อเสียของ linked list, fragmentation จาก heap vector จำนวนมาก, และต้นทุนการสร้าง·ทำลายที่ช้า แค่
Dropอย่างเดียวก็ใช้เวลารันไปส่วนใหญ่แล้วtree สามารถทำให้เป็นเชิงเส้นได้เสมอ จึงควรพิจารณารูปแบบการเข้าถึง·การแทรก, ว่าจริง ๆ เป็น tree หรือเป็น graph แบบอื่น, และจะเก็บเป็น
Vecหรือ struct ที่มีหลายVecผลลัพธ์คือโค้ดเร็วขึ้นและเรียบง่ายขึ้น ข้อมูลรวมอยู่ในอาร์เรย์ชนิดเดียวกัน ทำให้ใช้การ optimize SIMD ของคอมไพเลอร์และ L1 cache ได้ง่ายขึ้น และเมื่อจำเป็นก็สามารถเขียน โค้ด SIMD แบบไร้ branch เองได้เอกสารที่เกี่ยวข้อง: https://hn.algolia.com/?dateRange=all&page=0&prefix=true&que...
ในจุดคอขวดจริง ๆ ควรหลีกเลี่ยงการจัดสรรหน่วยความจำ, การค้นหา virtual function table, และการอ้างอิงทางอ้อมที่มากเกินไป แม้แต่
vectorของ C++ ก็ไม่ได้ดีที่สุดเสมอไป หากอาจเรียกการจัดสรรโดยไม่คาดคิดในทางกลับกัน โค้ดแบบ data-oriented แทบจะรองรับ threading และ SIMD ได้ง่ายเสมอ
ที่น่าสนใจคือท้ายที่สุดโค้ด CPU ก็จะถูกเขียนในสไตล์ GPU และวิธีหนึ่งคือใช้ struct of arrays แบบ Parquet แทน array of objects