แยกวิเคราะห์ JSON เร็วขึ้นด้วย Intel AVX-512

ภาพบุคคล2018facebook.jpg

โปรเซสเซอร์ Intel ล่าสุดจำนวนมากได้รับประโยชน์จากชุดคำสั่งใหม่ที่เรียกว่า AVX-512 คำแนะนำเหล่านี้ทำงานบนรีจิสเตอร์กว้าง (สูงสุด 512 บิต) และปฏิบัติตาม กระบวนทัศน์คำสั่งเดียว หลายข้อมูล (SIMD) คำแนะนำ AVX-512 ใหม่เหล่านี้ทำให้คุณสามารถทำลายสถิติความเร็วบางอย่างได้ เช่น การถอดรหัสข้อมูล base64 ด้วย ความเร็วของการคัดลอกหน่วยความจำ

โปรเซสเซอร์ที่ทันสมัยส่วนใหญ่มีคำสั่ง SIMD คำสั่ง AVX-512 นั้นกว้างกว่า (จำนวนบิตมากขึ้นต่อการลงทะเบียน) แต่นั่นก็ไม่จำเป็นว่าจะต้องดึงดูดใจหลัก หากคุณเพียงแค่ใช้อัลกอริธึม SIMD ที่มีอยู่และนำไปใช้กับ AVX-512 คุณอาจไม่ได้รับประโยชน์มากเท่าที่คุณต้องการ เป็นความจริงที่การลงทะเบียนที่กว้างขึ้นนั้นมีประโยชน์ แต่ในโปรเซสเซอร์ superscalar (โปรเซสเซอร์ที่สามารถออกคำสั่งได้หลายคำสั่งต่อรอบ) จำนวนคำสั่งที่คุณสามารถออกต่อรอบมีความสำคัญมากถ้าไม่มาก โดยทั่วไปแล้ว คำสั่ง AVX-512 แบบ 512 บิตจะมีราคาแพงกว่า และโปรเซสเซอร์สามารถออกคำสั่งเหล่านี้ได้น้อยลงต่อรอบ เพื่อให้ได้รับประโยชน์อย่างเต็มที่จาก AVX-512 คุณต้องออกแบบโค้ดของคุณอย่างระมัดระวัง การที่ Intel กำลังเผยแพร่คำสั่งเหล่านี้อย่างค่อยเป็นค่อยไปทำให้มีความท้าทายมากขึ้น: โปรเซสเซอร์ล่าสุดมีคำสั่ง AVX-512 อันทรงพลังใหม่จำนวนมากซึ่งไม่สามารถใช้งานได้ในตอนแรก ดังนั้น AVX-512 จึงไม่ใช่ “สิ่งเดียว” แต่เป็นชุดคำสั่งในตระกูล

นอกจากนี้ การปรับใช้คำสั่ง AVX-512 ในระยะแรกมักจะนำไปสู่การดาวน์คล็อกที่วัดได้: โปรเซสเซอร์จะลดความถี่ลงชั่วขณะหนึ่งหลังจากใช้คำแนะนำเหล่านี้ โชคดีที่โปรเซสเซอร์ Intel รุ่นล่าสุดที่สนับสนุน AVX-512 (Rocket Lake และ Ice Lake) ได้ยุติการควบคุมความถี่ที่เป็นระบบ นี้ โชคดีที่ตรวจพบโปรเซสเซอร์ล่าสุดเหล่านี้ได้ง่ายในขณะใช้งานจริง

เซิร์ฟเวอร์ Intel อันทรงพลังของ Amazon นั้นใช้ Ice Lake ดังนั้น หากคุณกำลังปรับใช้ซอฟต์แวร์แอปพลิเคชันของคุณกับคลาวด์บนเซิร์ฟเวอร์ที่ทรงพลัง แสดงว่าคุณอาจได้รับการสนับสนุนที่ดีสำหรับ AVX-512 แล้ว !

ไม่กี่ปีที่ผ่านมา เราได้เปิดตัว ตัวแยกวิเคราะห์ C++ JSON ที่เร็วจริงๆ ชื่อ simdjson ตัวแยกวิเคราะห์ค่อนข้างพิเศษเนื่องจากต้องอาศัยคำแนะนำ SIMD อย่างยิ่ง ในหลาย ๆ ตัวชี้วัด ตัวแยกวิเคราะห์ JSON นั้นเคยเป็นและยังคงเป็นตัวแยกวิเคราะห์ JSON ที่เร็วที่สุด แม้ว่าจะมีคู่แข่งที่น่าสนใจรายอื่นๆ เกิดขึ้น

ตอนแรกฉันได้เขียนเคอร์เนล AVX-512 ที่รวดเร็วและสกปรกสำหรับ simdjson เราไม่เคยรวมมันเข้าด้วยกันและหลังจากนั้นไม่นานฉันก็ลบมันทิ้งไป แล้วฉันก็ลืมมันไป

ขอบคุณการมีส่วนร่วมจากวิศวกรของ Intel มากความสามารถ (Fangzheng Zhang และ Weiqiang Wan) รวมถึงการมีส่วนร่วมทางอ้อมจากผู้อ่านบล็อกนี้ (Kim Walisch และ Jatin Bhateja) เราจึงผลิตเคอร์เนล AVX-512 ใหม่ขึ้นมา และเช่นเคย โปรดจำไว้ว่า simdjson เป็นผลงานของคนจำนวนมาก ซึ่งเป็นชุมชนทั้งหมดที่มีผู้มีส่วนร่วมหลายสิบคน ฉันต้องแสดงความขอบคุณต่อ Fangzheng Zhang ที่เขียนถึงฉันเกี่ยวกับพอร์ต AVX-512 ก่อน

เราเพิ่งเปิดตัวในเวอร์ชันล่าสุดของ simdjson มันทำลายสถิติความเร็วใหม่

ให้เราพิจารณาการทดสอบที่น่าสนใจที่คุณต้องการสแกนทั้งไฟล์ (ขนาดกิโลไบต์) เพื่อค้นหาค่าที่สอดคล้องกับตัวระบุบางตัว ใน simdjson รหัสมีดังนี้:

 auto doc = parser . วนซ้ำ ( json ) ;       
   สำหรับ ( ทวีต อัตโนมัติ : doc . find_field ( " สถานะ " ) ) {   
      if ( uint64_t ( ทวีต . find_field ( " id " ) ) = = find_id ) {   
        ผลลัพธ์ = ทวี  find_field ( " ข้อความ " ) ;   
        คืนค่า จริง ;   
      }   
    }   
    คืนค่า เท็จ ;   

บนโปรเซสเซอร์ Tiger Lake ด้วย GCC 11 ฉันได้รับความเร็วในการประมวลผลเป็นสองเท่า โดยแสดงด้วยจำนวนไบต์อินพุตที่ประมวลผลต่อวินาที

simdjson (512-bit SIMD): ใหม่ 7.4 GB/วินาที
simdjson (256 บิต SIMD): เก่า 4.6 GB/วินาที

ความเร็วที่เพิ่มขึ้นนั้นสำคัญมากเพราะในงานนี้ เรามักจะอ่านข้อมูลเป็นหลัก และประมวลผลรองค่อนข้างน้อย เราไม่ได้สร้างต้นไม้จากข้อมูล JSON เราไม่ได้สร้างโครงสร้างข้อมูล

ไลบรารี simdjson มีฟังก์ชันย่อขนาดซึ่งเพิ่งตัดช่องว่างที่ไม่จำเป็นออกจากอินพุต อาจเป็นเรื่องน่าประหลาดใจที่เราเร็วกว่าสองเท่าของค่าพื้นฐานก่อนหน้า:

simdjson (512-bit SIMD): ใหม่ 12 GB/วินาที
simdjson (256 บิต SIMD): เก่า 4.3 GB/วินาที

เกณฑ์มาตรฐานที่สมเหตุสมผลอีกประการหนึ่งคือการแยกวิเคราะห์อินพุตลงในแผนผัง DOM ด้วยการตรวจสอบแบบเต็ม การแยกวิเคราะห์ไฟล์ JSON มาตรฐาน ( twitter.json ) ฉันได้รับเกือบ 30%:

simdjson (512-bit SIMD): ใหม่ 3.6 GB/วินาที
simdjson (256 บิต SIMD): เก่า 2.8 GB/วินาที

แม้ว่า 30% อาจฟังดูไม่น่าตื่นเต้น แต่เราเริ่มต้นจากเส้นฐานที่รวดเร็ว

เราทำได้ดีกว่านี้ไหม? ได้อย่างมั่นใจ มี AVX-512 มากมายที่เรายังไม่ได้ใช้งาน เราไม่ใช้การดำเนินการบูลีนแบบไตรภาค ( vpternlog ) เราไม่ได้ใช้ฟังก์ชันสับเปลี่ยนอันทรงพลังใหม่ (เช่น vpermt2b ) เรามีตัวอย่างของวิวัฒนาการร่วมกัน: ฮาร์ดแวร์ที่ดีกว่าต้องการซอฟต์แวร์ใหม่ ซึ่งจะทำให้ฮาร์ดแวร์มีความโดดเด่น

แน่นอน เพื่อให้ได้ประโยชน์ใหม่เหล่านี้ คุณต้องมีโปรเซสเซอร์ Intel รุ่นล่าสุดที่มีการรองรับ AVX-512 ที่เพียงพอ และแน่นอนว่าคุณต้องมีโปรเซสเซอร์ C++ ที่ค่อนข้างใหม่ด้วยเช่นกัน โปรเซสเซอร์ Intel ระดับแล็ปท็อปรุ่นล่าสุดบางตัวไม่รองรับ AVX-512 แต่คุณน่าจะใช้ได้หากคุณพึ่งพา AWS และมีโหนด Intel ขนาดใหญ่

คุณสามารถคว้า รุ่นของเราได้โดยตรง หรือรอให้มันไปถึงหนึ่งในตัวจัดการแพ็คเกจมาตรฐาน (MSYS2, conan, vcpkg, brew, debian, FreeBSD เป็นต้น)

หลีกเลี่ยงการส่งข้อยกเว้นในโค้ดที่ไวต่อประสิทธิภาพ

ภาพบุคคล2018facebook.jpg

มีหลายวิธีในซอฟต์แวร์ในการจัดการเงื่อนไขข้อผิดพลาด ใน C หรือ Go รหัสหนึ่งส่งคืนรหัสข้อผิดพลาด ภาษาโปรแกรมอื่น ๆ เช่น C++ หรือ Java ต้องการส่ง ข้อยกเว้น ข้อดีอย่างหนึ่งของการใช้ข้อยกเว้นคือช่วยให้โค้ดของคุณสะอาดเป็นส่วนใหญ่ เนื่องจากโค้ดจัดการข้อผิดพลาดมักจะแยกจากกัน

เป็นที่ถกเถียงกันว่าการจัดการข้อยกเว้นนั้นดีกว่าการจัดการกับรหัสข้อผิดพลาดหรือไม่ ฉันจะใช้อย่างใดอย่างหนึ่งอย่างมีความสุข

สิ่งที่ฉันจะคัดค้านคือการใช้ข้อยกเว้นสำหรับโฟลว์การควบคุม เป็นเรื่องปกติที่จะโยนข้อยกเว้นเมื่อไม่สามารถเปิดไฟล์ได้โดยไม่คาดคิด แต่คุณไม่ควรใช้ข้อยกเว้นเพื่อแยกประเภทของค่า

ให้ฉันอธิบาย

สมมติว่ารหัสของฉันคาดว่าจำนวนเต็มจะเป็นบวกเสมอ ฉันอาจมีฟังก์ชันที่ตรวจสอบเงื่อนไขดังกล่าว:

 int get_positive_value ( int x ) {   
    if ( x < 0 ) { throw std :: runtime_error ( " ไม่เป็นค่าบวก! " ) ; }   
    ผลตอบแทน x ;   
}   

จนถึงตอนนี้ดีมาก ฉันถือว่าปกติแล้วข้อยกเว้นจะไม่ถูกส่งออกไป มันจะถูกโยนทิ้งถ้าฉันมีข้อผิดพลาดบางอย่าง

ถ้าฉันต้องการรวมค่าสัมบูรณ์ของจำนวนเต็มที่มีอยู่ในอาร์เรย์ โค้ดการโยงหัวข้อต่อไปนี้ก็ใช้ได้:

 int sum = 0 ;   
    สำหรับ ( int x : a ) {   
        ถ้า ( x < 0 ) {   
            ผลรวม + = - x ;   
        } อื่นๆ {   
            ผลรวม + = x ;   
        }   
    }   
   

น่าเสียดายที่ฉันมักจะเห็นวิธีแก้ปัญหาที่ใช้ข้อยกเว้นในทางที่ผิด:

 int sum = 0 ;   
    สำหรับ ( int x : a ) {   
        ลอง {   
            ผลรวม + = get_positive_value ( x ) ;   
        } จับ ( . . . ) {   
            ผลรวม + = - x ;   
        }   
    }   

เห็นได้ชัดว่าโค้ดหลังนี้น่าเกลียดและดูแลรักษายาก แต่ที่มากไปกว่านั้นก็อาจไม่มีประสิทธิภาพมากนัก เพื่อแสดงให้เห็น ฉันเขียนเบนช์มาร์กขนาดเล็กบนอาร์เรย์สุ่มที่มีองค์ประกอบสองสามพัน รายการ ฉันใช้คอมไพเลอร์ LLVM clang 12 บนโปรเซสเซอร์ skylake รหัสปกติเร็วกว่า 10,000 เท่าในการทดสอบของฉัน!

รหัสปกติ 0.05 ns/ค่า
ข้อยกเว้น 500 ns/ค่า

ผลลัพธ์ของคุณจะแตกต่างออกไป แต่โดยทั่วไปแล้ว การใช้ข้อยกเว้นสำหรับโฟลว์การควบคุมจะนำไปสู่ประสิทธิภาพที่ไม่เหมาะสม แถมยังน่าเกลียดอีกด้วย!

ทุ่นลอยมีความแม่นยำ 15 หลักในช่วงปกติ

ภาพบุคคล2018facebook.jpg

ในภาษาการเขียนโปรแกรม เช่น JavaScript หรือ Python โดยทั่วไปตัวเลขจะแสดงโดยใช้ประเภทตัวเลข IEEE 64 บิต (binary64) สำหรับตัวเลขเหล่านี้ เรามีความแม่นยำ 15 หลัก หมายความว่าคุณสามารถเลือกตัวเลข 15 หลักได้ เช่น 1.23456789012345e100 และสามารถแสดงได้อย่างแม่นยำ: มีตัวเลขทศนิยมที่มีตัวเลขนัยสำคัญ 15 หลักเหล่านี้พอดี ในกรณีนี้คือหมายเลข 6355009312518497 * 2 280 .

เห็นได้ชัดว่ามันล้มเหลวสำหรับตัวเลขที่อยู่นอกช่วงที่ถูกต้อง ตัวอย่างเช่น ตัวเลข 1,500 มีขนาดใหญ่เกินไป และไม่สามารถแสดงโดยตรงโดยใช้ตัวเลขทศนิยม 64 บิตแบบมาตรฐาน ในทำนองเดียวกัน 1e-500 นั้นเล็กเกินไปและสามารถแสดงเป็นศูนย์ได้เท่านั้น

ช่วงของเลขทศนิยม 64 บิต อาจถูกกำหนดเป็นไปจาก 4.94e-324 ถึง 1.8e308 และ -1.8e308 ถึง -4.94e-324 ร่วมกับ 0 เท่านั้น อย่างไรก็ตาม ช่วงนี้รวมถึงตัวเลขต่ำกว่าปกติที่มีความแม่นยำสัมพัทธ์ สามารถมีขนาดเล็ก ตัวอย่างเช่น หมายเลข 5.00000000000000e-324 แสดงได้ดีที่สุดเป็น 4.94065645841247e-324 ซึ่งหมายความว่าเรามีความแม่นยำเป็นศูนย์

เพื่อให้กฎความแม่นยำ 15 หลักทำงาน คุณอาจยังคงอยู่ในช่วงปกติ เช่น จาก 2.225e−308 ถึง 1.8e308 และ -1.8e308 ถึง -2.225e−308 มีเหตุผลที่ดีอื่นๆ ที่จะยังคงอยู่ในช่วงปกติ เช่น ประสิทธิภาพต่ำและความแม่นยำต่ำในช่วงต่ำกว่าปกติ