Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

เมื่อ 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:

  1. A Compiled Review
  2. Concurrency benchmark computing sum of squares in C, C++, Rust, and Go