ทำ CSV Parsing ได้ 21 GB/s ด้วย SIMD บน AMD 9950X
(nietras.com)- Sep 0.10.0 ทำความเร็วได้ 21 GB/s ในการ parse CSV ระดับต่ำ ด้วยการปรับแต่งสำหรับ CPU ที่รองรับ AVX-512 เช่น AMD 9950X (Zen 5) เร็วขึ้นจากเดิมราว 18 GB/s
- ประสิทธิภาพที่ดีขึ้นมาจากการเปลี่ยนโครงสร้าง parser เพื่อลดคอขวดจาก การไป-กลับของ mask register ในการสร้างโค้ด AVX-512 ของ .NET 9.0
- AVX-512-to-256 parser ใหม่โหลด
charแบบ 512 บิต แล้วแปลงเป็นเวกเตอร์ไบต์ 256 บิต เพื่อหลีกเลี่ยงต้นทุนของการจัดการ mask และการ permute แยกต่างหาก - ประสิทธิภาพการ parse ระดับต่ำของ Sep ดีขึ้น ราว 3 เท่า จากประมาณ 7 GB/s บน 5950X/.NET 7.0 ในเวอร์ชัน 0.1.0 เมื่อปี 2023 เป็นประมาณ 21 GB/s บน 9950X/.NET 9.0 ในเวอร์ชัน 0.10.0
- ใน benchmark ระดับสูง Sep แบบ multithread บน 9950X ก็ประมวลผล package assets 1 ล้านแถวได้ใน 72.213ms หรือราว 8.0 GB/s และข้อมูล floats ก็ทำได้ราว 8.1 GB/s
เป้าหมายและผลลัพธ์ของ Sep 0.10.0
- Sep 0.10.0 ออกเมื่อวันที่ 22 เมษายน 2025 โดยรวมการปรับแต่งสำหรับ CPU ที่รองรับ AVX-512 เช่น AMD 9950X (Zen 5) และ benchmark บน 9950X
- เมื่อวัดจากการ parse CSV ระดับต่ำ Sep ทำได้ 21 GB/s บน 9950X
- ก่อนเวอร์ชัน 0.10.0 ทำได้ประมาณ 18 GB/s บน 9950X เครื่องเดียวกัน
- ขอบเขตการวิเคราะห์คือการ parse
Rowsระดับต่ำของข้อมูล CSV package assets และตัวเลขทั้งหมดอ้างอิงแบบ เธรดเดียว - ตัวเลข benchmark อาจแกว่งได้ไม่กี่เปอร์เซ็นต์พอยต์ จึงอาจเห็น regression เล็กน้อยในบาง release
การเปลี่ยนแปลงประสิทธิภาพตั้งแต่ 0.1.0 ถึง 0.10.0
- ประสิทธิภาพของ Sep ค่อย ๆ ดีขึ้นจากผลร่วมของการเปลี่ยนโค้ด การเปลี่ยนเวอร์ชัน .NET และการเปลี่ยนเจเนอเรชัน CPU
- ลำดับประสิทธิภาพหลักมีดังนี้
- 0.1.0, 5950X, .NET 7.0: ประมาณ 7 GB/s
- 0.3.0, 5950X, .NET 8.0: ประมาณ 12 GB/s
- 0.6.0, 5950X, .NET 9.0: ประมาณ 13 GB/s
- 0.9.0, 9950X, .NET 9.0: ประมาณ 18 GB/s
- 0.10.0, 9950X, .NET 9.0: ประมาณ 21 GB/s
- หลังจาก Sep เปิดตัวในเดือนมิถุนายน 2023 ผ่านไปไม่ถึง 2 ปีเล็กน้อย ก็เร็วขึ้นราว 3 เท่า
- หากเทียบกรณีใช้ Sep 0.9.0 บน 5950X กับใช้ Sep 0.10.0 บน 9950X จะดีขึ้นราว 1.6 เท่า
- boost clock ของ 9950X คือ 5.7GHz ส่วน 5950X คือ 4.9GHz
- มองว่าความต่างของ clock นี้เพียงอย่างเดียวอธิบายได้ราว 1.2 เท่า
คอขวดของ mask register ในการสร้างโค้ด .NET AVX-512
- Sep รองรับ AVX-512 ตั้งแต่ 0.2.3 แต่ .NET 8 ในเวลานั้นยังไม่รองรับ mask register
k1-k8ของ AVX-512 แบบชัดเจน - ในการสร้างโค้ด AVX-512 เดิม ผลลัพธ์จากการ compare จะเข้าไปอยู่ใน mask register จากนั้นถูกย้ายไป general register แล้วจึงกลับมาเข้า mask register อีกครั้ง
- หลังอัปเกรดเป็น 9950X แล้ว Sep 0.9.0 ทำได้ประมาณ 18 GB/s ในการ parse CSV ระดับต่ำ ซึ่งเร็วกว่าบน 5950X ราว 1.4 เท่า
- เมื่อเปลี่ยน parser ผ่าน environment variable เพื่อเปรียบเทียบ พบว่า AVX2 parser บน 9950X ทำได้ประมาณ 20 GB/s เร็วกว่า AVX-512 parser เดิมราว 10%
- ความต่างนี้ยืนยันว่าการจัดการ mask register ของ AVX-512 ยังคงส่งผลต่อประสิทธิภาพ
โครงสร้างพื้นฐานของลูป parsing ใน Sep
- parser ทุกตัวของ Sep ใช้โครงสร้างพื้นฐานเดียวกัน และรองรับสองเส้นทางที่ต่างกันตามการจัดการเครื่องหมายอัญประกาศผ่าน generic method
ParseเพียงตัวเดียวParseColInfos: ใช้เมื่อมีการจัดการเครื่องหมายอัญประกาศ และต้องติดตาม state มากกว่าParseColEnds: ใช้เมื่อไม่มีการจัดการเครื่องหมายอัญประกาศ
- การ parse ทำเป็นหน่วย span ของ
charที่นำมาจาก array โดยตัวอย่างมีขนาด 16K- ขนาดนี้เล็กพอที่จะอยู่ใน CPU cache และยังเอื้อต่อ multithreading ที่มีประสิทธิภาพในภายหลัง
- ลูปโหลดข้อมูลอักขระ 16 บิตเข้า SIMD register แปลงเป็น byte SIMD register แล้ว compare กับอักขระพิเศษของ CSV
- ตัวที่ compare รวมถึง
\n,\r,",;เป็นต้น
- ตัวที่ compare รวมถึง
- ผลการ compare ถูกแปลงเป็น bitmask แล้ว parse เฉพาะบิตที่ถูกตั้งค่าใน mask ตามลำดับ
- ความต่างด้านประสิทธิภาพขึ้นอยู่มากกับว่าโค้ด SIMD C# นี้ถูก JIT compile เป็น machine code แบบใดใน .NET
AVX-512 parser เดิมและการปรับใน 0.10.0
SepParserAvx512PackCmpOrMoveMaskTzcntใน 0.9.0 โหลดchar32 ตัวลงใน SIMD register 512 บิตสองตัว จากนั้น pack เป็น byte vector 512 บิตหนึ่งตัว เพื่อประมวลผล 64 อักขระ ต่อหนึ่งลูป- ข้อมูลที่ถูก pack มีลำดับปะปนกัน จึงต้องจัดเรียงใหม่ด้วย
PermuteVar8x64 - ใน assembly ของ .NET 9.0 แต่ละ
Vec.Equalsกลายเป็นคำสั่งvpcmpeqbและvpmovm2bสองคำสั่ง โดยมีการย้ายระหว่าง mask register เช่นk1กับ general vector registerzmmซ้ำ ๆ - ใน Sep 0.10.0 มีการเลื่อนการเรียก
MoveMaskให้เร็วขึ้น เพื่อลด จำนวนครั้งของการไป-กลับ ระหว่าง mask register กับ general register- ใน parser อื่น ๆ จะเรียก
MoveMaskเฉพาะเมื่อจำเป็น เพื่อลดจำนวนคำสั่งใน fast path กรณี “ไม่มีอักขระพิเศษ”
- ใน parser อื่น ๆ จะเรียก
- แม้หลังปรับแล้วยังมีการย้ายจาก mask register ไป general register อยู่ แต่จำนวนคำสั่ง assembly โดยรวมลดลง
AVX2 และ AVX-512-to-256 parser ใหม่
- assembly ของ
SepParserAvx2PackCmpOrMoveMaskTzcntที่ใช้ AVX2 มีโครงสร้างตรงไปตรงมากว่า เพราะไม่มี mask register - ด้วยโครงสร้างนี้ AVX2 parser จึงเร็วกว่า AVX-512 parser เดิมใน 0.9.0
SepParserAvx512To256CmpOrMoveMaskTzcntตัวใหม่ใน 0.10.0 โหลดcharด้วยคำสั่ง AVX-512 แล้วสร้าง byte vector 256 บิตด้วยConvertToVector256ByteWithSaturation- คำสั่งจริงคือ
vpmovuswb - throughput ต่อหนึ่งลูปคือ
char“เพียง” 32 ตัว แต่โครงสร้างเรียบง่ายกว่า
- คำสั่งจริงคือ
- วิธีนี้หลีกเลี่ยงปัญหา mask register แบบ 512 บิต และข้อมูลที่ pack แล้วอยู่ในลำดับถูกต้องใน
ymm4อยู่แล้ว จึงไม่ต้องมี permute แยกต่างหาก - parser ใหม่ช่วยดันประสิทธิภาพการ parse ของ Sep บน 9950X ขึ้นไปถึงราว 21 GB/s
benchmark ระดับต่ำบน 9950X แยกตาม parser
- ผลการรัน parser ทั้งหมดผ่าน environment variable บน AMD 9950X เพื่อเปรียบเทียบ พบว่า AVX-512-to-256 parser ใหม่เร็วที่สุด
- ผลหลักมีดังนี้
SepParserAvx512To256CmpOrMoveMaskTzcnt: 21597.7 MB/s, 27.0 ns/row, 1.351msSepParserVector256NrwCmpExtMsbTzcnt: 20608.5 MB/s, 28.3 ns/row, 1.416msSepParserAvx2PackCmpOrMoveMaskTzcnt: 20599.3 MB/s, 28.3 ns/row, 1.417msSepParserAvx512PackCmpOrMoveMaskTzcnt: 19944.3 MB/s, 29.3 ns/row, 1.463ms
- parser ข้ามแพลตฟอร์มที่ใช้
Vector256ทำได้ใกล้เคียงกับ AVX2 มาก - parser ข้ามแพลตฟอร์มที่ใช้
Vector128และVector512ยังเร็วอยู่ แต่ช้ากว่า 5~10% และVector512ช้ากว่าVector128 SepParserIndexOfAnyตามหลังอยู่มากที่ 2787.0 MB/s และVector64ไม่ถูกเร่งความเร็วบน 9950X จึงอยู่ที่ 459.9 MB/s
benchmark ระดับสูงของ 5950X และ 9950X
- ในข้อมูล package assets ผลการประมวลผล 1 ล้านแถวบน 9950X เร็วกว่า 5950X อย่างมาก
- 5950X
Sep_MT: 119.430ms, 4888.1 MB/s - 9950X
Sep_MT: 72.213ms, 8084.1 MB/s
- 5950X
- บน 9950X
Sepแบบเธรดเดียวประมวลผล package assets 1 ล้านแถวได้ใน 291.979ms, 1999.4 MB/s - ใน benchmark package assets เดียวกันบน 9950X ตัวเปรียบเทียบมีประสิทธิภาพดังนี้
Sylvan: 413.265ms, 1412.6 MB/sReadLine_: 377.033ms, 1548.4 MB/s, allocation 1991.04MBCsvHelper: 1005.323ms, 580.7 MB/s
- ในข้อมูล floats เช่นกัน Sep แบบ multithread บน 9950X ประมวลผล 25,000 แถวได้ใน 2.497ms, 8136.8 MB/s
- การปรับปรุงใน benchmark ระดับสูงจาก 5950X ไป 9950X อยู่ที่ราว 1.5~1.6 เท่า ใกล้เคียงกับ benchmark ระดับต่ำ
ยังไม่มีความคิดเห็น