การเพิ่มประสิทธิภาพระดับล่างและ Zig
(alloc.dev)- การเพิ่มประสิทธิภาพระดับล่าง สามารถทำได้อย่างสะดวกในภาษา Zig
- แม้ว่าโดยมากแล้ว คอมไพเลอร์ จะทำการปรับแต่งประสิทธิภาพได้ดี แต่บางครั้งการสื่อเจตนาของโปรแกรมเมอร์ให้ชัดเจนก็ช่วยให้ได้ประสิทธิภาพที่ดีกว่า
- Zig รองรับการสร้างโค้ดประสิทธิภาพสูงและเมตาโปรแกรมมิงที่ทรงพลังด้วยความสามารถ comptime (การทำงานขณะคอมไพล์)
- เมื่อเทียบกับ Rust แล้ว Zig สามารถปรับแต่งได้ละเอียดกว่า ผ่าน annotation และโครงสร้างโค้ดที่เขียนอย่างชัดเจน
- ในงานที่ต้องทำซ้ำอย่าง การเปรียบเทียบสตริง สามารถใช้ comptime เพื่อสร้างโค้ดแอสเซมบลีที่ดีกว่าฟังก์ชันทั่วไปได้
การเพิ่มประสิทธิภาพและ Zig
อย่างที่มีคำเตือนอันโด่งดังว่า “ทุกอย่างเป็นไปได้ แต่สิ่งที่น่าสนใจนั้นไม่ได้มาอย่างง่ายดาย” การเพิ่มประสิทธิภาพของโปรแกรม จึงเป็นเรื่องที่นักพัฒนาสนใจอยู่เสมอ ไม่ว่าจะเพื่อควบคุมต้นทุนของโครงสร้างพื้นฐานคลาวด์ ลด latency หรือทำให้ระบบเรียบง่ายขึ้น การปรับแต่งโค้ดให้มีประสิทธิภาพ เป็นสิ่งจำเป็น บทความนี้จะอธิบายแนวคิดของการเพิ่มประสิทธิภาพระดับล่างใน Zig และจุดแข็งของ Zig เป็นหลัก
เราจะเชื่อใจคอมไพเลอร์ได้หรือไม่?
- โดยทั่วไปมักมีคำแนะนำว่า “เชื่อใจคอมไพเลอร์” แต่ในความเป็นจริงก็มีกรณีที่ คอมไพเลอร์ทำงานต่างจากที่คาด หรือแม้กระทั่งละเมิดข้อกำหนดของภาษา
- ภาษาระดับสูงมีข้อจำกัดด้านประสิทธิภาพ เพราะสื่อ เจตนา (intent) ได้ไม่ชัดเจน
- ภาษาระดับล่างทำให้คอมไพเลอร์รู้ข้อมูลที่จำเป็นต่อการปรับแต่งได้จาก ความชัดเจนของโค้ด ตัวอย่างเช่นเมื่อเปรียบเทียบฟังก์ชัน
maxArrayใน JavaScript กับ Zig นั้น Zig สามารถส่งข้อมูลเรื่องชนิดข้อมูล การจัดแนว และการมี alias หรือไม่ ให้ทราบตั้งแต่ตอนคอมไพล์แทนที่จะไปรู้ตอนรันไทม์ - หากเขียนการทำงาน
maxArrayแบบเดียวกันด้วย Zig และ Rust ก็จะได้โค้ดแอสเซมบลีประสิทธิภาพสูงที่แทบเหมือนกัน แต่ ยิ่งแสดงเจตนาได้ดีเท่าไร ผลลัพธ์ของการปรับแต่งก็ยิ่งดีขึ้น - อย่างไรก็ตาม เราไม่อาจเชื่อมั่นในประสิทธิภาพของคอมไพเลอร์ได้เสมอไป ดังนั้นในจุดคอขวดจึงควร ตรวจดูทั้งโค้ดและผลลัพธ์จากการคอมไพล์ด้วยตนเอง แล้วหาวิธีปรับแต่งเพิ่มเติม
บทบาทของ Zig
- Zig สามารถสร้าง โค้ดที่ผ่านการปรับแต่งอย่างดีโดยไม่ต้องพึ่งข้อมูลเชิงนามธรรม ได้ ด้วยคุณสมบัติอย่าง ความชัดเจนที่แม่นยำ ฟังก์ชัน built-in ที่หลากหลาย pointer และ annotation, comptime และ Illegal Behavior ที่นิยามไว้อย่างชัดเจน
- Rust รับประกันโดยพื้นฐานจาก memory model ว่าอาร์กิวเมนต์จะไม่ alias กัน แต่ใน Zig ผู้พัฒนาต้อง ใส่ annotation อย่าง noalias ด้วยตนเอง
- หากพิจารณาจาก LLVM IR เพียงอย่างเดียว ระดับการปรับแต่งของ Zig ก็ถือว่าสูง
- ที่สำคัญที่สุดคือ comptime (การทำงานขณะคอมไพล์) ของ Zig เป็นเครื่องมือเพิ่มประสิทธิภาพที่ทรงพลังมาก
comptime คืออะไร?
- comptime ของ Zig ถูกนำไปใช้ในการสร้างโค้ด การฝังค่า constant และการสร้าง generic struct ตามชนิดข้อมูล ซึ่งมีบทบาทสำคัญต่อการเพิ่มประสิทธิภาพขณะรันไทม์
- สามารถใช้ comptime เพื่อทำ เมตาโปรแกรมมิง ได้
- ต่างจาก macro ใน C/C++ หรือระบบ macro ของ Rust ตรงที่ comptime เป็น โค้ดปกติ ไม่ใช่ไวยากรณ์แยกต่างหาก
- โค้ด comptime ไม่ได้แก้ไข AST โดยตรง แต่สามารถตรวจสอบ สะท้อนผล และสร้างสิ่งต่าง ๆ สำหรับทุกชนิดข้อมูลได้ในช่วงคอมไพล์
- ความยืดหยุ่นของ comptime ยังส่งอิทธิพลต่อการพัฒนาของภาษาอื่นอย่าง Rust ด้วย และถูกผสานเข้ากับภาษา Zig อย่างเป็นธรรมชาติ
ข้อจำกัดของ comptime
- ความสามารถของ macro บางอย่าง เช่น token-pasting ไม่สามารถแทนที่ด้วย comptime ของ Zig ได้
- Zig ให้ความสำคัญกับ ความอ่านง่ายของโค้ด จึงไม่อนุญาตให้สร้างตัวแปรหรือกำหนด macro แบบออกนอกขอบเขต
- แต่ในทางกลับกัน Zig comptime ก็มีตัวอย่างการใช้งานด้านเมตาโปรแกรมมิงอย่างกว้างขวาง เช่น type reflection, การสร้าง DSL และการปรับแต่งการ parse สตริงให้มีประสิทธิภาพ
การเพิ่มประสิทธิภาพการเปรียบเทียบสตริงด้วย comptime
- ฟังก์ชันเปรียบเทียบสตริงแบบทั่วไปนั้นสามารถเขียนได้ในทุกภาษา แต่ใน Zig หากสตริงหนึ่งในสองตัวเป็น ค่าคงที่ที่รู้ตั้งแต่ตอน comptime ก็สามารถสร้างโค้ดแอสเซมบลีที่มีประสิทธิภาพกว่าได้
- ตัวอย่างเช่น หากสตริงหนึ่งเป็น
"Hello!\n"เสมอ ก็สามารถใช้การปรับแต่งแบบเปรียบเทียบเป็น บล็อกขนาดใหญ่กว่าในระดับไบต์ แทนการเทียบทีละไบต์ - โดยใช้ comptime เราสามารถสร้างโค้ดประสิทธิภาพสูงตั้งแต่ตอนคอมไพล์ได้ ไม่ว่าจะเป็น เวกเตอร์ SIMD การประมวลผลแบบบล็อก หรือการปรับแต่งสำหรับไบต์ที่เหลือ
- แนวทางนี้ไม่ได้ใช้ได้แค่กับการเปรียบเทียบสตริงซ้ำ ๆ เท่านั้น แต่ยังใช้สร้างโค้ดที่เน้นประสิทธิภาพสำหรับ แมปปิงจากข้อมูลคงที่ ตาราง perfect hash และตัวแยกวิเคราะห์ AST ได้อีกหลากหลาย
บทสรุป
- Zig เหมาะอย่างยิ่งกับการเพิ่มประสิทธิภาพระดับล่าง และด้วยโครงสร้างโค้ดที่ชัดเจนรวมถึงความสามารถ comptime ที่ทรงพลัง ทำให้สามารถลงมือสร้างประสิทธิภาพสูงสุดได้โดยตรง
- เมื่อเทียบกับภาษาอื่นอย่าง Rust ความสามารถด้านการเขียนโปรแกรมขณะคอมไพล์และความชัดเจนของ Zig เป็นข้อได้เปรียบสำคัญในการพัฒนาซอฟต์แวร์ประสิทธิภาพสูง
- ความสามารถด้านการเพิ่มประสิทธิภาพของ Zig จะยิ่งกลายเป็นจุดแข็งในการแข่งขันที่สำคัญมากขึ้นในอนาคต
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
for(;;);ต้องเป็นลูปไม่สิ้นสุดจริง ๆ และloop {}ของ Rust ก็ควรเป็นแบบเดียวกัน แต่บางครั้งนักพัฒนา LLVM เหมือนจะเผลอคิดว่าตัวเองทำแต่คอมไพเลอร์ C++ พอ Rust บอกว่า "ขอลูปอนันต์นะ" LLVM ก็กลับใช้ตรรกะแบบ "ใน C++ ไม่มีทางเป็นแบบนั้น งั้น optimize เลย!" แล้วปัญหาก็เกิด เท่ากับเอา optimization ที่ผิดไปใช้กับภาษาที่ผิดi < x.lengthกันอยู่แล้ว ทำให้ JIT optimize ได้ มุมนี้เลยอาจเป็นการจับผิดนิดหน่อย แม้จะเป็นความต่างเล็ก ๆ ก็ตามpurchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;เป็นโค้ดที่เต็มไปด้วยเจตนา แต่คาดเดาไม่ได้เลยว่าสุดท้าย machine code จะออกมาเป็นอย่างไร-march=native, whole-program optimization ฯลฯ) จริง ๆ แล้วฝั่ง C ก็ใช้ optimization hint อย่างunreachableผ่าน language extension ได้ และ Clang ก็ทำ constant folding อย่างดุดันมากเหมือนกัน พูดอีกแบบคือ ความต่างระหว่าง comptime ของ Zig กับ codegen ของ C หลายครั้งมาจากการตั้งค่า optimization ของคอมไพเลอร์ TL;DR: ถ้า C ช้า ให้ตรวจสอบการตั้งค่าคอมไพเลอร์ก่อน เพราะหัวใจของ optimization ยังไงก็เป็น LLVM อยู่ดีforloop ของ Zig ดูรกรุงรังเกินไป ต้องเอาลิสต์สองชุดมาวางขนานกันและจัดตำแหน่งให้ตรงกัน แค่มองก็ปวดตาแล้ว ผมคิดว่าภาษายุคหลัง ๆ พลาดตรงที่ยัด syntax แบบ 'เวทมนตร์' และสัญลักษณ์พิเศษมากเกินไป ดูแล้วคงทนจ้องหลายชั่วโมงไม่ไหว