• 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 ถูกแปลงเป็น bitmask แล้ว parse เฉพาะบิตที่ถูกตั้งค่าใน mask ตามลำดับ
  • ความต่างด้านประสิทธิภาพขึ้นอยู่มากกับว่าโค้ด SIMD C# นี้ถูก JIT compile เป็น machine code แบบใดใน .NET

AVX-512 parser เดิมและการปรับใน 0.10.0

  • SepParserAvx512PackCmpOrMoveMaskTzcnt ใน 0.9.0 โหลด char 32 ตัวลงใน SIMD register 512 บิตสองตัว จากนั้น pack เป็น byte vector 512 บิตหนึ่งตัว เพื่อประมวลผล 64 อักขระ ต่อหนึ่งลูป
  • ข้อมูลที่ถูก pack มีลำดับปะปนกัน จึงต้องจัดเรียงใหม่ด้วย PermuteVar8x64
  • ใน assembly ของ .NET 9.0 แต่ละ Vec.Equals กลายเป็นคำสั่ง vpcmpeqb และ vpmovm2b สองคำสั่ง โดยมีการย้ายระหว่าง mask register เช่น k1 กับ general vector register zmm ซ้ำ ๆ
  • ใน Sep 0.10.0 มีการเลื่อนการเรียก MoveMask ให้เร็วขึ้น เพื่อลด จำนวนครั้งของการไป-กลับ ระหว่าง mask register กับ general register
    • ใน parser อื่น ๆ จะเรียก MoveMask เฉพาะเมื่อจำเป็น เพื่อลดจำนวนคำสั่งใน fast path กรณี “ไม่มีอักขระพิเศษ”
  • แม้หลังปรับแล้วยังมีการย้ายจาก 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.351ms
    • SepParserVector256NrwCmpExtMsbTzcnt: 20608.5 MB/s, 28.3 ns/row, 1.416ms
    • SepParserAvx2PackCmpOrMoveMaskTzcnt: 20599.3 MB/s, 28.3 ns/row, 1.417ms
    • SepParserAvx512PackCmpOrMoveMaskTzcnt: 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
  • บน 9950X Sep แบบเธรดเดียวประมวลผล package assets 1 ล้านแถวได้ใน 291.979ms, 1999.4 MB/s
  • ใน benchmark package assets เดียวกันบน 9950X ตัวเปรียบเทียบมีประสิทธิภาพดังนี้
    • Sylvan: 413.265ms, 1412.6 MB/s
    • ReadLine_: 377.033ms, 1548.4 MB/s, allocation 1991.04MB
    • CsvHelper: 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 ระดับต่ำ

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น