เมื่อ Benchmark เผยปรัชญาของภาษา: มุมมองเรื่องความถูกต้องใน Rust
1. บทนำ — ทำไมเรื่องนี้ถึงสำคัญ
บางครั้ง Benchmark ไม่ได้วัดแค่ความเร็วของการทำงาน แต่กลับเผยให้เราเห็นถึงปรัชญาเบื้องลึกในการออกแบบของภาษานั้นๆ
หากเรามองเผินๆ การทดสอบเปรียบเทียบความเร็วในการประมวลผลระหว่าง C, C++, Go และ Rust โดยให้แต่ละภาษาทำงานเดียวกัน คือคำนวณผลรวมของกำลังสองของจำนวนเต็มตั้งแต่ 1 ถึง 100 ล้าน แบ่งการทำงานออกเป็น 4 เธรด ดูเหมือนจะเป็นเพียงการแข่งความเร็วธรรมดา ทั้งที่โค้ดส่วนใหญ่แบ่งพาร์ทิคิชั่นและลูปเหมือนกันเป๊ะ ผลลัพธ์ที่ถูกต้องคือ “672,921,401,752,298,880” แต่อะไรทำให้ Rust ดูเหมือนจะสอบตก ทั้งคำนวณช้าและยังให้ผลลัพธ์ที่ผิดเพี้ยน? หรือว่านี่คือความตั้งใจของภาษา?
2. แนวคิดพื้นฐาน — Mental Model
เมื่อพิจารณาให้ลึกลงไป ปัญหาที่ซ่อนอยู่ในโจทย์นี้คือ ขนาดของตัวเลขที่ใช้ในการคำนวณ แม้ว่าค่าแต่ละพจน์ ($i^2$) จะยังอยู่ในขอบเขตของจำนวนเต็ม 64 บิต แต่เมื่อเริ่มนำค่าจำนวนมากมาสะสมรวมกัน ผลรวมสุดท้ายได้ข้ามขีดจำกัดสูงสุดไปเสียแล้วโดยที่นักพัฒนาไม่รู้ตัว
และที่จุดเกิด Integer Overflow นี่เองทำให้ภาษาต่างๆ แสดง “บุคลิก” ออกมาอย่างชัดเจน:
- C และ C++: ให้อิสระสูงสุดแก่ผู้พัฒนา การใช้
long longทำให้เกิด signed integer overflow ซึ่งตามมาตรฐานของภาษาถือเป็น Undefined Behavior (พฤติกรรมที่ไม่ถูกกำหนด) Compiler สามารถเลือกทำอะไรก็ได้กับโค้ดชุดนี้ การไม่มีคำเตือนไม่ได้แปลว่าโค้ดนั้นถูกต้องตามหลักคณิตศาสตร์ - Go: เน้นความคาดเดาได้ โดยใช้
uint64ที่กำหนดพฤติกรรมตอน overflow ไว้อย่างชัดเจนว่าเป็นการวนรอบ (wrap-around) ตามโมดูลัส $2^{64}$ โปรแกรมทำงานจบและไม่แจ้ง Error ใดๆ แต่ผลลัพธ์ที่ได้อาจไม่ใช่ความหมายทางคณิตศาสตร์ที่ต้องการ
The Rust Philosophy
Rust ปฏิเสธที่จะ “เดาแทนผู้พัฒนา” ว่าการ Overflow นั้นเป็นสิ่งที่ยอมรับได้หรือไม่ สิ่งนี้สะท้อนว่า Rust เลือกให้ความสำคัญกับความถูกต้องเหนือความสะดวกสบายหรือความเร็วล้วนๆ
3. เจาะลึกกลไกภายใน — How It Works Under the Hood
3.1 กลไกป้องกัน Overflow ของ Rust
Rust กลับเลือกเดินคนละเส้นทางอย่างชัดเจน ภาษาไม่ยอมให้การ overflow ผ่านไปแบบเงียบๆ โดยไม่ตั้งคำถาม เมื่อใช้ชนิดข้อมูล 64 บิตในการสะสมค่าขนาดมหึมา การคำนวณนี้จะถูกดักจับได้ทันที
- โหมด Debug (
cargo run): โปรแกรมจะ Panic และหยุดทำงานทันทีเมื่อเกิด Overflow - โหมด Release (
cargo run --release): จะเกิดการ Wrap-around (Two’s complement wrap) เพื่อประสิทธิภาพ แต่สิ่งนี้ถือเป็นเจตนาและการตัดสินใจที่ผู้พัฒนาต้องควบคุมได้
Key Insight: Explicit is better than Implicit
เพื่อให้ได้ผลลัพธ์ที่ถูกต้อง ผู้พัฒนาจำเป็นต้องเปลี่ยนจากการปล่อยให้เงียบไป มาเป็นการระบุชนิดข้อมูลให้ใหญ่พอตั้งแต่แรก เช่น การใช้
u128เพื่อรองรับผลรวมระดับ $6.7 \times 10^{17}$ อย่างถูกต้อง
3.2 เมื่อระบุ Type ได้ถูกต้อง
เมื่อเลือกใช้ Type อย่าง u128 ได้ถูกต้องแล้ว Rust จะรันได้อย่างปลอดภัยและให้ผลลัพธ์ทางคณิตศาสตร์ที่ตรงกับความเป็นจริงตลอดรอดฝั่ง โดยไม่พึ่งพา Undefined Behavior หรือการ Wrap แบบแอบแฝง
#![allow(unused)]
fn main() {
// ตัวอย่างการเปลี่ยนมาใช้ u128 เพื่อป้องกันผลรวมล้น
let mut sum: u128 = 0;
for i in 1..=100_000_000 {
// Cast เป็น u128 ก่อนเพื่อสะสมค่า
sum += (i as u128) * (i as u128);
}
}
4. ผลกระทบต่อ Ecosystem
ทัศนคติที่มีต่อความถูกต้องเหนือสิ่งอื่นใดของ Rust สร้างวัฒนธรรมในชุมชนที่เน้นการตรวจสอบขอบเขตข้อมูลอย่างเข้มงวด Library (Crates) หลายตัวถูกออกแบบมาโดยบังคับให้จัดการ Error หรืองานตัวเลขอย่างรัดกุม การใช้ checked_add, saturating_add, หรือ wrapping_add กลายมาเป็น Standard Pattern ที่ถูกเรียกใช้อย่างแพร่หลาย เพื่อระบุเจตนาให้ผู้อ่านโค้ดรับรู้ในทันทีว่าจะรับมือกับตัวเลขที่เกินลิมิตอย่างไร
5. ข้อควรระวังและ Trade-offs
ความเข้มงวดนี้ย่อมมีต้นทุนของตัวเอง:
The Cost of Correctness
หากผู้เขียนโค้ดเพิ่งย้ายมาจาก C++ หรือ Go อาจรู้สึกอึดอัดที่โค้ดไม่คอมไพล์ หรือรันแล้วระเบิดเป็น Panic อยู่บ่อยครั้ง
Trade-off ที่ชัดเจนที่สุดคือ Developer Experience (DX) ในระยะแรก โค้ดจะดูเทอะทะขึ้นเพราะผู้พัฒนาจะต้องประกาศ Type, Cast ข้อมูล หรือจัดการกับ Option/Result จาก checked_* methods อยู่เสมอ แต่ถ้าคิดในระยะยาว มันหมายถึงระบบที่มีบั๊กเชิงคณิตศาสตร์น้อยชิ้นที่สุด และไม่เกิดปัญหาประหลาดตอนนำขึ้น Production แล้วโดน Load ทะลัก
6. บทสรุป
การทดสอบนี้จึงไม่ใช่การแข่งขันเรื่องความเร็วอีกต่อไป แต่เป็นภาพสะท้อนของปรัชญาการออกแบบภาษา C และ C++ ให้อิสระแลกกับความเสี่ยง Go เรียบง่ายแต่ก็ละเลยผลลัพธ์ทางคณิตศาสตร์ ส่วน Rust เลือกที่จะเป็นคนเจ้าระเบียบ
Key Takeaway
Benchmark นี้ไม่ได้แสดงว่า Rust ด้อยประสิทธิภาพกว่าชาวบ้าน แต่มันแสดงให้เห็นว่า Rust เลือกที่จะไม่แลกความถูกต้องทางการคำนวณกับความสะดวก เมื่อระบบถูกกดดันด้วยขนาดของข้อมูล ความแตกต่างเชิงปรัชญานี้เองที่ฉายแววเด่นชัดออกมา
แต่ละภาษาก็มีความงดงามในแบบของตนเอง คำถามสุดท้ายคือ เมื่อต้องรับผิดชอบระบบงานของคุณ คุณจะเลือกความเงียบหรือเลือกความถูกต้อง?
Credit & Reference: