ข้าม​ไป​ยัง​เนื้อหา

file เดียว compiler ตัว​เดียว สอง​รหัส​ออก

ออ​เด​อร์​ใบ​หนึ่ง​มี​ยอด​รวม 10 และ​มี​ยอด​คืน​เงิน​ที่​ตาม​มา​ทีหลัง​เป็น 25 คู่​นี้​ผิด​ตั้งแต่​ต้นทาง แต่​มัน​เดิน​มา​ถึง service ของ​คุณ​จริง เพราะ​ไม่มี​ใคร​ดัก​มัน​ไว้​ก่อน​หน้า​นั้น

service คำนวณ​ยอด​ที่​ยัง​ต้อง​เก็บ​ด้วย total - refunded ทั้ง​สอง​ค่า​เป็น u32 และ​ทั้ง​สอง​ค่า มา​ถึง​ตอน runtime ไม่​ได้​เขียน​ค้าง​ไว้​ใน source

คำถาม​ของ​บท​นี้​ไม่ใช่​ว่า​ผลลัพธ์​ควร​เป็น​เท่าไร แต่​เป็น​ว่า binary ที่​คุณ​เทสต์ กับ binary ที่​คุณ​ส่ง​ขึ้น production ตอบ​เหมือน​กัน​หรือ​เปล่า คำ​ตอบ​ที่​วัด​ได้​คือ​ไม่

📦 orderctl — ของ​ชิ้น​เดียวกัน​กับ​คอร์ส​ภาษา​อื่น​ใน​เสา​นี้

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ ระหว่าง​นี้​ทุก​ตัวเลข​ใน​บท​นี้​รัน​เอง​ได้​ด้วย npm run verify:integer-semantics ซึ่ง​เรียก python3 scripts/integer-semantics/probe.py · ไม่​ต้อง​มี Docker และ​ไม่​ต้อง​ต่อ​เน็ต

source ชุด​เดียว compiler ตัว​เดียว ต่าง​กัน​ที่​ธง​ตัว​เดียว แล้ว​ได้​รหัส​ออก​คนละ​ค่า

ค่า​ทั้ง​สอง​มา​จาก argv ซึ่ง​เป็น​เงื่อนไข​ที่​ขาด​ไม่​ได้ ถ้า​เขียน​สอง​ค่า​นี้​เป็น literal rustc จะ​ปฏิเสธ​ตั้งแต่ compile และ​ไม่มี binary ออก​มา​ให้​รัน​สัก​ฝั่ง หัวข้อ​สุดท้าย​ของ​บท​นี้​วัด​กรณี​นั้น​ไว้

scripts/integer-semantics/runners/a.rs
// ค่าทั้งสองมาจาก 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ผล​ของ​การ​คำนวณ​ที่​เกิน​ช่วง​ของ​ชนิด​ปลาย​ทาง · ภาษา​เลือก​ทำ​อย่าง​ใด​อย่าง​หนึ่ง​ใน​สาม​อย่าง คือ​หยุด​โปรแกรม วน​กลับ​เข้า​ช่วง หรือ​คืน​สัญญาณ​ว่า​ทำ​ไม่​ได้ และ​สอง​อย่าง​หลัง​ไม่​ส่ง​เสียง​อะไร​เลย และ​บท​นี้​ยก​มา​เฉพาะ​บรรทัด​นั้น

บรรทัด​ข้อความ​ของ panic
attempt to subtract with overflow

บรรทัด​ที่​อยู่​เหนือ​มัน​มี​หมายเลข​ของ thread ซึ่ง​เปลี่ยน​ทุก​ครั้ง​ที่​รัน probe จึง​ผูก​ไว้​ที่​บรรทัด ข้อความ​บรรทัด​นี้​บรรทัด​เดียว

ฝั่ง​ที่​ทำงาน​จน​จบ​ไม่​ได้​เตือน​อะไร​สัก​คำ มัน​คืน​ยอด​ที่​ยัง​ต้อง​เก็บ​เป็น 4294967281 แล้ว​ส่ง​ต่อ ให้​ขั้น​ถัด​ไป​ของ​ท่อ​เหมือน​ค่า​ปกติ​ค่า​หนึ่ง

เจ็ด​แถว​นี้​หัก​ล้าง​คำ​ว่า debug panic แล้ว release wrap ได้​ทั้ง​สอง​ครึ่ง

ทุก​แถว​ใช้ a.rs file เดิม และ​สั่ง​ด้วย 10 กับ 25 เหมือน​กัน​หมด ต่าง​กัน​ที่​ธง​อย่าง​เดียว

คำ​สั่งรหัส​ออกstdout
rustc a.rs101(panic)
rustc -O a.rs04294967281
rustc -C debug-assertions=off a.rs04294967281
rustc -O -C debug-assertions=on a.rs101(panic)
rustc -O -C overflow-checks=on a.rs101(panic)
rustc -C overflow-checks=off a.rs04294967281
rustc -C opt-level=3 a.rs04294967281

สาม​แถว​ใน​ตาราง​นี้​พัง​คำ​อธิบาย​ที่​ติดปาก​กัน​อยู่

  • แถว​ที่ 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 ไม่​ได้​ตัดสิน​อะไร​ด้วย​ตัว​เอง

หน้า 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 แล้ว​การ​ทดลอง​จะ​เขียว​ทั้ง​สอง​ฝั่ง โดย​ไม่​ได้​วัด​อะไร​เลย

คน​ที่​อยาก​ลอง​เอง​มัก​จะ​ตัด​ส่วน​ที่​อ่าน argv ทิ้ง แล้ว​เขียน​ค่า​ลง​ไป​ตรงๆ เพื่อ​ให้​ทดลอง​ง่าย ผล​คือ​โปรแกรม​คนละ​ตัว​กับ​ที่​ตั้งใจ​จะ​วัด

scripts/integer-semantics/runners/b.rs
fn main() { let x: u32 = 10u32 - 25u32; println!("{}", x); }

rustc b.rs ไม่​ได้​ให้ binary ที่ panic ออก​มา มัน​หยุด​ตั้งแต่ compile

stderr ของ rustc b.rs
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 ผลที่วัดได้คืออะไร