file เดียว compiler ตัวเดียว สองรหัสออก
ออเดอร์ใบหนึ่งมียอดรวม 10 และมียอดคืนเงินที่ตามมาทีหลังเป็น 25 คู่นี้ผิดตั้งแต่ต้นทาง
แต่มันเดินมาถึง service ของคุณจริง เพราะไม่มีใครดักมันไว้ก่อนหน้านั้น
service คำนวณยอดที่ยังต้องเก็บด้วย total - refunded ทั้งสองค่าเป็น u32 และทั้งสองค่า
มาถึงตอน runtime ไม่ได้เขียนค้างไว้ใน source
คำถามของบทนี้ไม่ใช่ว่าผลลัพธ์ควรเป็นเท่าไร แต่เป็นว่า binary ที่คุณเทสต์ กับ binary ที่คุณส่งขึ้น production ตอบเหมือนกันหรือเปล่า คำตอบที่วัดได้คือไม่
orderctl คือเครื่องมือประมวลผลออเดอร์ที่คอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในบทนี้รันเองได้ด้วย npm run verify:integer-semantics
ซึ่งเรียก python3 scripts/integer-semantics/probe.py · ไม่ต้องมี Docker และไม่ต้องต่อเน็ต
source ชุดเดียว กับสองรหัสออก
หัวข้อที่มีชื่อว่า “source ชุดเดียว กับสองรหัสออก”source ชุดเดียว compiler ตัวเดียว ต่างกันที่ธงตัวเดียว แล้วได้รหัสออกคนละค่า
ค่าทั้งสองมาจาก argv ซึ่งเป็นเงื่อนไขที่ขาดไม่ได้ ถ้าเขียนสองค่านี้เป็น literal rustc
จะปฏิเสธตั้งแต่ compile และไม่มี binary ออกมาให้รันสักฝั่ง หัวข้อสุดท้ายของบทนี้วัดกรณีนั้นไว้
// ค่าทั้งสองมาจาก argv · คอมไพเลอร์จึงพับค่าล่วงหน้าไม่ได้fn main() { let v: Vec<u32> = std::env::args().skip(1).map(|s| s.parse().unwrap()).collect(); let total = v[0]; let refunded = v[1]; println!("{}", total - refunded);}compile สองครั้ง แล้วสั่งด้วยอาร์กิวเมนต์ 10 กับ 25 เท่ากันทั้งสองครั้ง
rustc a.rsได้รหัสออก 101 และไม่มีอะไรออกทาง stdout เลยrustc -O a.rsได้รหัสออก 0 และ stdout เป็น4294967281
ฝั่งที่หยุดทำงานพิมพ์ stderr ออกมาหลายบรรทัด บรรทัดที่บอกเหตุคือบรรทัดของ การล้นของจำนวนเต็มinteger overflowผลของการคำนวณที่เกินช่วงของชนิดปลายทาง · ภาษาเลือกทำอย่างใดอย่างหนึ่งในสามอย่าง คือหยุดโปรแกรม วนกลับเข้าช่วง หรือคืนสัญญาณว่าทำไม่ได้ และสองอย่างหลังไม่ส่งเสียงอะไรเลย และบทนี้ยกมาเฉพาะบรรทัดนั้น
attempt to subtract with overflowบรรทัดที่อยู่เหนือมันมีหมายเลขของ thread ซึ่งเปลี่ยนทุกครั้งที่รัน probe จึงผูกไว้ที่บรรทัด ข้อความบรรทัดนี้บรรทัดเดียว
ฝั่งที่ทำงานจนจบไม่ได้เตือนอะไรสักคำ มันคืนยอดที่ยังต้องเก็บเป็น 4294967281 แล้วส่งต่อ
ให้ขั้นถัดไปของท่อเหมือนค่าปกติค่าหนึ่ง
ธงเจ็ดทรง วัดทีละทรง
หัวข้อที่มีชื่อว่า “ธงเจ็ดทรง วัดทีละทรง”เจ็ดแถวนี้หักล้างคำว่า debug panic แล้ว release wrap ได้ทั้งสองครึ่ง
ทุกแถวใช้ a.rs file เดิม และสั่งด้วย 10 กับ 25 เหมือนกันหมด ต่างกันที่ธงอย่างเดียว
| คำสั่ง | รหัสออก | stdout |
|---|---|---|
rustc a.rs | 101 | (panic) |
rustc -O a.rs | 0 | 4294967281 |
rustc -C debug-assertions=off a.rs | 0 | 4294967281 |
rustc -O -C debug-assertions=on a.rs | 101 | (panic) |
rustc -O -C overflow-checks=on a.rs | 101 | (panic) |
rustc -C overflow-checks=off a.rs | 0 | 4294967281 |
rustc -C opt-level=3 a.rs | 0 | 4294967281 |
สามแถวในตารางนี้พังคำอธิบายที่ติดปากกันอยู่
- แถวที่ 4 กับแถวที่ 5 optimize แล้ว และยัง panic ทั้งคู่มี
-Oอยู่ในคำสั่ง - แถวที่ 6 ไม่ได้ optimize เลย และยังวนกลับ มันให้
4294967281เท่ากับแถวที่ 2
แถวที่ 7 ไม่ได้หักล้างอะไร มันระบุ opt-level=3 มาตรงๆ แล้วให้ผลเท่ากับแถวที่ 2 ทุกช่อง
ซึ่งเป็นผลที่คำอธิบายเดิมทายถูก แถวนี้จึงบอกว่าคำอธิบายเดิมทายถูกได้เป็นบางแถว ไม่ใช่ทุกแถว
สวิตช์จริงจึงเป็น -C overflow-checks-C overflow-checksธงของ rustc ที่ตัดสินว่าการล้นจะ panic หรือวนกลับ · เปิดแล้วได้รหัสออก 101 ปิดแล้วได้รหัสออก 0 · มันตั้งฉากกับการ optimize สนิท build ที่ optimize แล้วจึงยัง panic ได้ และมันตั้งฉากกับการ optimize สนิท
ระดับของ opt-level ไม่ได้ตัดสินอะไรด้วยตัวเอง
แล้วทำไมคนถึงจำว่าเป็น debug กับ release
หัวข้อที่มีชื่อว่า “แล้วทำไมคนถึงจำว่าเป็น debug กับ release”หน้า codegen options ของ rustc เขียนกฎค่าตั้งต้นไว้สองข้อ ซึ่งต่อกันเป็นสายเดียว
- ถ้าไม่ระบุ
-C overflow-checksมันจะเปิดเมื่อ -C debug-assertions-C debug-assertionsธงที่ overflow-checks หยิบไปใช้เป็นค่าตั้งต้นเมื่อไม่ได้ระบุ · มันเปิดเองที่ opt-level=0 เท่านั้น ซึ่งเป็นเหตุผลเดียวที่ทำให้ debug กับ release ดูเหมือนเป็นสวิตช์ ทั้งที่เป็นผลพลอยได้ เปิด และปิดเมื่อ debug-assertions ปิด - ถ้าไม่ระบุ
-C debug-assertionsมันจะเปิดเองก็ต่อเมื่อopt-levelเป็น0เท่านั้น
ต่อสองข้อนี้เข้าด้วยกันแล้วได้ผลว่า build ที่ไม่ optimize จะ panic และ build ที่ optimize จะวนกลับ ซึ่งตรงกับที่คนจำไว้พอดี
แต่นั่นคือปลายทางของสายที่ต่อจากกฎสองข้อนี้ ไม่ใช่กฎ ตัดสายตรงไหนก็ได้ด้วยการระบุธงเอง แล้วผลลัพธ์จะไม่ตรงกับที่จำไว้ทันที ซึ่งคือสิ่งที่แถวที่ 4 ถึงแถวที่ 6 แสดงให้เห็น
flowchart TD
A["ระบุ -C overflow-checks มาไหม"] -->|ระบุ| B["ใช้ค่าที่ระบุ"]
A -->|ไม่ระบุ| C["ระบุ -C debug-assertions มาไหม"]
C -->|ระบุ| D["ใช้ค่าของ debug-assertions"]
C -->|ไม่ระบุ| E["opt-level เป็น 0 ไหม"]
E -->|เป็น 0| F["debug-assertions เปิดเอง"]
E -->|ไม่ใช่ 0| G["debug-assertions ปิดเอง"]
B --> H{"overflow-checks เปิดหรือปิด"}
D --> H
F --> H
G --> H
H -->|เปิด| I["panic · รหัสออก 101"]
H -->|ปิด| J["วนกลับ · รหัสออก 0 · 4294967281"]
คำบรรยายภาพ: การตัดสินใจเกิดที่ช่องล่างสุดช่องเดียวเสมอ ส่วน opt-level เข้ามาเกี่ยวก็ต่อเมื่อ ไม่ได้ระบุธงทั้งสองตัว จึงอยู่ปลายสายที่ยาวที่สุด ไม่ใช่ต้นสาย
debug กับ release เป็นชื่อของ profile ไม่ใช่ชื่อของสวิตช์ คนที่จำปลายสายนี้เป็น
กฎข้อเดียว จะไปหาสวิตช์ผิดตัวตอนของขึ้น production แล้วยอดไม่ตรง
ราคาที่ต้องจ่ายเพื่อไม่ให้พลาดคือต้องระบุ -C overflow-checks เองในทุก profile ที่คุณสนใจ
ซึ่งแปลว่า build ที่ optimize แล้วก็ยังต้องยอมจ่ายค่าตรวจทุกการคำนวณ
กับดักของ literal
หัวข้อที่มีชื่อว่า “กับดักของ literal”เขียนค่าเดียวกันเป็น literal แล้วการทดลองจะเขียวทั้งสองฝั่ง โดยไม่ได้วัดอะไรเลย
คนที่อยากลองเองมักจะตัดส่วนที่อ่าน argv ทิ้ง แล้วเขียนค่าลงไปตรงๆ เพื่อให้ทดลองง่าย
ผลคือโปรแกรมคนละตัวกับที่ตั้งใจจะวัด
fn main() { let x: u32 = 10u32 - 25u32; println!("{}", x); }rustc b.rs ไม่ได้ให้ binary ที่ panic ออกมา มันหยุดตั้งแต่ compile
error: this arithmetic operation will overflow --> b.rs:1:26 |1 | fn main() { let x: u32 = 10u32 - 25u32; println!("{}", x); } | ^^^^^^^^^^^^^ attempt to compute `10_u32 - 25_u32`, which would overflow | = note: `#[deny(arithmetic_overflow)]` on by default
error: aborting due to 1 previous error#[deny(arithmetic_overflow)] เปิดอยู่เป็นค่าตั้งต้น คำว่า deny แปลว่าระดับของ lint ตัวนี้
เป็น error ไม่ใช่คำเตือน compiler จึงหยุดแทนที่จะเดินต่อ
สิ่งที่วัดได้จาก b.rs มีสองค่า และทั้งสองค่าเท่ากันทั้ง rustc b.rs กับ rustc -O b.rs
- รหัสออกเป็น 1 ทั้งสองฝั่ง ซึ่งเป็นรหัสของการ compile ไม่ผ่าน ไม่ใช่ 101 ของ panic
- ไม่มี binary ออกมาเลยสักฝั่ง จึงไม่มีอะไรให้รันต่อ และไม่มี stdout ให้เทียบ
การทดลองที่ใช้ literal อย่างเดียวจึงรายงานว่า สองฝั่งตรงกัน ซึ่งเป็นความจริง แต่มันตรงกัน ที่การไม่มี binary ไม่ใช่ที่ผลลัพธ์ของการคำนวณ
นี่คือรูปของการวัดที่เขียวด้วยเหตุผลผิด ค่าที่รายงานถูกต้องทุกตัว แต่สิ่งที่มันวัดไม่ใช่สิ่งที่ คำถามถาม และไม่มีอะไรในรายงานบอกความต่างข้อนี้
bug ตัวจริงเข้าถึงได้ก็ต่อเมื่อค่าข้ามพรมแดนของ runtime มาแล้วเท่านั้น พรมแดนนั้นอยู่ที่
argvของ process- request body ที่เข้ามาทาง HTTP
- ข้อความที่อ่านออกมาจาก queue
- แถวที่อ่านออกมาจาก database
ทุกทางเข้าในรายการนี้คือที่ที่เงินจริงเดินทางมา และไม่มีทางไหนที่ compiler มองเห็นค่าล่วงหน้าได้
จุดแรกในห้าจุด
หัวข้อที่มีชื่อว่า “จุดแรกในห้าจุด”บทนี้ปิดจุดที่หนึ่ง อีกสี่จุดที่เหลืออยู่ในเจ็ดบทถัดไป และทุกจุดเงียบเหมือนกัน
ธีสิสของคอร์สนี้เป็นประโยคเดียว คือ ค่าจำนวนเต็มค่าหนึ่งเปลี่ยนได้ห้าจุดระหว่างทางผ่านระบบ และทุกจุดในห้าจุดนั้นเงียบ
จุดที่หนึ่งคือจุดที่บทนี้เพิ่งวัด คือตอนความกว้างของชนิดปลายทางหมดลง สิ่งที่บอกว่าเกิดอะไรขึ้น ไม่ใช่ชื่อภาษา ไม่ใช่ชื่อ profile แต่เป็นธงที่คุณไม่ได้ตั้ง
บทที่ 2 ถามคำถามเดียวกันกับอีกห้าภาษา และคำถามที่แยกภาษาออกจากกันไม่ใช่ว่าภาษาไหน ตอบเท่าไร แต่เป็นว่าภาษานั้นมีความกว้างให้หมดตั้งแต่แรกหรือเปล่า
-
The rustc book — Codegen options (เข้าถึง 2026-09-12) — ที่มาของกฎค่าตั้งต้นสองข้อ คือ
overflow-checksที่ตามค่าของdebug-assertionsและdebug-assertionsที่เปิดเองเมื่อopt-levelเป็น0 -
The Rust Reference — Operator expressions, Overflow (เข้าถึง 2026-09-12) — ที่มาของรายการการคำนวณที่นับว่าล้น และของข้อความที่ว่าธง
-C debug-assertionsกับ-C overflow-checksใช้ควบคุมเรื่องนี้ได้ตรงกว่า -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/integer-semantics/probe.pyซึ่งสั่งrustcตรงๆ ไม่ผ่าน cargo แล้ววัดทั้งชุดสามรอบต่อการเรียกหนึ่งครั้ง · วัดบนrustc 1.97.1บน darwin/arm64 เมื่อ 2026-09-12
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3สั่ง `rustc -O -C overflow-checks=on a.rs` แล้วรันด้วย 10 กับ 25 ผลที่วัดได้คืออะไร