ฟังก์ชัน `tolower()` ที่ใช้ AVX-512
(dotat.at)-
เมื่อหลายปีก่อน ผู้เขียนเคยเขียนบทความเกี่ยวกับวิธีทำให้
tolower()ทำงานได้เร็วขึ้นด้วยเทคนิค SWAR ไม่กี่วันก่อน ได้สนใจบทความของ Olivier Giniaux ที่อธิบายวิธีเพิ่มประสิทธิภาพการประมวลผลสตริงสั้นด้วยคำสั่ง SIMD วิธีนี้ถูกใช้ในฟังก์ชันแฮชความเร็วสูงที่เขียนด้วย Rust -
คำสั่ง SIMD สามารถจัดการสตริงสั้นได้ง่าย แต่สิ่งที่ทำให้รู้สึกติดขัดมาตลอดคือการย้ายข้อมูลระหว่างหน่วยความจำกับเวกเตอร์รีจิสเตอร์ บทความของ Olivier นำเสนอวิธีที่น่าสนใจในการแก้ปัญหานี้
สัญญาณแห่งความหวัง
-
ชุดคำสั่ง SIMD บางชุดมีความสามารถโหลดและจัดเก็บแบบ masked ที่มีประโยชน์สำหรับการประมวลผลสตริง โดยทำงานในระดับไบต์
- ARM SVE: ใช้ได้บนคอร์ ARM Neoverse รุ่นใหญ่รุ่นใหม่ เช่น Amazon Graviton แต่ใช้ไม่ได้บน Apple Silicon
- AVX-512-BW: ใช้ได้บนโปรเซสเซอร์ AMD Zen รุ่นใหม่ AVX-512 เป็นชุดส่วนขยายที่ซับซ้อน และบน Intel การรองรับค่อนข้างไม่แน่นอน
-
ผู้เขียนมีเครื่อง AMD Zen 4 อยู่ จึงตัดสินใจลองใช้ AVX-512-BW
tolower64()
- ผู้เขียนใช้คู่มือ intrinsics ของ Intel เพื่อเขียนฟังก์ชัน
tolower()พื้นฐานที่ประมวลผลได้ครั้งละ 64 ไบต์- ใช้
*เป็น wildcard เพื่อค้นหาmm512*epi8และหาฟังก์ชัน AVX-512 ระดับไบต์ - เติมหลายรีจิสเตอร์ด้วยไบต์ที่มีประโยชน์ 64 ตัว
- กำหนดค่าตัวเลขที่จำเป็นสำหรับแปลงตัวพิมพ์ใหญ่เป็นตัวพิมพ์เล็ก
- เปรียบเทียบอักขระขาเข้ากับ A และ Z เพื่อตรวจว่าเป็นตัวพิมพ์ใหญ่หรือไม่
- ใช้ mask เพื่อแปลงเป็นตัวพิมพ์เล็กเมื่อเป็นตัวพิมพ์ใหญ่
- ใช้
การโหลดและจัดเก็บแบบจำนวนมาก
- ต้องห่อ kernel
tolower64()ด้วยฟังก์ชันที่ใช้งานสะดวกขึ้น เช่น ฟังก์ชันที่คัดลอกสตริงพร้อมแปลงเป็นตัวพิมพ์เล็กไปด้วย- สำหรับสตริงยาว จะใช้คำสั่งโหลดและจัดเก็บเวกเตอร์แบบไม่ต้องจัดแนว
การโหลดและจัดเก็บแบบ masked
- สำหรับสตริงสั้นและช่วงท้ายของสตริงยาว จะใช้การโหลดและจัดเก็บแบบไม่ต้องจัดแนวที่มี mask
- mask จะตั้งค่าบิต
lenตัวแรกไว้ - การโหลดและจัดเก็บจะคล้ายกับเวอร์ชันความกว้างเต็มที่เพิ่ม mask เข้าไป
- mask จะตั้งค่าบิต
การทำเบนช์มาร์ก
-
มีการทำเบนช์มาร์กเปรียบเทียบประสิทธิภาพของฟังก์ชันคล้ายกันหลายตัว
- คอมไพล์ด้วย Clang 16 และรันบน AMD Ryzen 9 7950X
- แต่ละฟังก์ชันคอมไพล์แยกกันเพื่อหลีกเลี่ยงผลรบกวนจาก inlining และการย้ายโค้ด
-
ผลลัพธ์:
tolower64เร็วที่สุดในบรรดาฟังก์ชันทั้งหมดที่ทดสอบcopybytes64ใช้ AVX-512 ในลักษณะคล้ายtolower64แต่ไม่ได้เร็วกว่าอย่างมากcopybytes1ทำmemcpyทีละไบต์ และแสดงให้เห็นว่า auto-vectorization ของ Clang 11 ค่อนข้างไม่ดีtolower()มาตรฐานช้าที่สุดtolower1คือtolower()แบบทีละไบต์ที่คอมไพล์ด้วย Clang 16 ซึ่ง auto-vectorization ดีขึ้น แต่ก็ยังช้าอยู่tolower8คือ SWARtolower()จากบล็อกโพสต์ก่อนหน้า โดย Clang พยายามทำ auto-vectorization แต่ผลลัพธ์ไม่ดีmemcpyช่วงแรกเร็ว แต่หลังจากนั้นความเร็วลดลงเหลือครึ่งหนึ่งของcopybytes64
บทสรุป
-
AVX-512-BW มีประโยชน์มาก โดยเฉพาะเมื่อต้องจัดการสตริงสั้น
-
บน Zen 4 มันเร็วมาก และ built-in functions ก็ใช้งานง่าย
-
ประสิทธิภาพของ AVX-512-BW ลื่นไหลมาก
-
ผู้เขียนไม่มีเครื่องที่รองรับ ARM SVE จึงยังตรวจสอบเชิงลึกไม่ได้ แต่สงสัยว่า SVE จะทำงานกับสตริงสั้นได้ดีแค่ไหน
-
ผู้เขียนหวังว่าการขยายชุดคำสั่งเหล่านี้จะถูกใช้อย่างแพร่หลายมากขึ้น เพราะจะช่วยเพิ่มประสิทธิภาพการประมวลผลสตริงได้มาก
-
โค้ดของบล็อกโพสต์นี้สามารถดูได้บนเว็บไซต์ของผู้เขียน
สรุปโดย GN⁺
- บทความนี้อธิบายวิธีประมวลผลสตริงสั้นอย่างมีประสิทธิภาพด้วยคำสั่ง SIMD
- แสดงให้เห็นว่าชุดคำสั่ง AVX-512-BW และ ARM SVE มีประโยชน์ต่อการประมวลผลสตริง
- ผลการทำเบนช์มาร์กชี้ว่า AVX-512-BW ให้ประสิทธิภาพโดดเด่น โดยเฉพาะกับสตริงสั้น
- บทความนี้น่าจะเป็นประโยชน์กับนักพัฒนาที่สนใจการเพิ่มประสิทธิภาพ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ในโมเดลหน่วยความจำของ Rust และ LLVM กลเม็ด "unsafe read beyond of death" ถือเป็นพฤติกรรมที่ไม่ถูกนิยาม
ทำให้อยากรู้เกี่ยวกับการใช้งาน AVX512 ของ AMD และการแข่งขันกับ AVX10 ของ Intel
การปรับแต่ง SWAR มีประโยชน์เฉพาะกับสตริงที่จัดแนวกับแอดเดรส 8 ไบต์เท่านั้น
การเพิ่ม mask ดูเรียบร้อยดี
หากใช้ Clang จะได้ผลลัพธ์ที่ดีกว่า
ลูปหลักสำหรับสตริงความยาวสั้นมีคำสั่งน้อยกว่าอยู่หนึ่งคำสั่ง
เคยเขียน implementation คล้ายกันใน C# สำหรับแปลงตัวพิมพ์ใหญ่/เล็กของ ASCII เป็น UTF-8
มีตัวอย่างการใช้ SIMD เพื่อแปลงข้อความให้เป็น uwu โดยใช้ AVX512
ถ้าคำนึงถึงการแปลงอักขระ Unicode ด้วยจะยิ่งน่าประทับใจกว่านี้
เคยมีประสบการณ์เพิ่มขอบสีดำรอบภาพเพื่อหลีกเลี่ยงปัญหา SIMD ของบัฟเฟอร์