โมเดล Stack Machine
WebAssembly ทำงานบนหลักการที่เรียบง่ายแต่ทรงพลัง: stack machine แทนที่จะใช้ register ที่มีชื่อเหมือน CPU ทั่วไป Wasm จะ push และ pop ค่าบน operand stack ที่ implicit เพื่อรันคำสั่งทุกตัว
Stack Machine คืออะไร
หัวข้อที่มีชื่อว่า “Stack Machine คืออะไร”ใน register machine คำสั่งจะระบุ register ต้นทางและปลายทางอย่างชัดเจน เช่น ADD R1, R2, R3 แต่ใน stack machine คำสั่งจะทำงานกับค่าบนสุดของ stack โดยปริยาย
ลำดับการทำงานสำหรับการบวก 3 + 4 มีดังนี้:
flowchart TD A["Stack: []\n> i32.const 3"] --> B["Stack: [3]\n> i32.const 4"] B --> C["Stack: [3, 4]\n> i32.add"] C --> D["Stack: [7]\n<- return value"]
i32.const 3— push ค่า 3 ลงบน stacki32.const 4— push ค่า 4 ลงบน stacki32.add— pop ค่า 2 ตัว (3 และ 4), บวกเข้าด้วยกัน, push ผลลัพธ์ (7) กลับ- ฟังก์ชันจบลง ค่า 7 บนสุดของ stack กลายเป็น return value
ทำไมต้องใช้ Stack Machine
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ Stack Machine”Stack machine มีข้อได้เปรียบหลายประการสำหรับ portable bytecode:
- ตรวจสอบได้ง่าย — type checker สามารถ walk stack statically และตรวจสอบความถูกต้องของ type ได้โดยไม่ต้องวิเคราะห์ที่ซับซ้อน
- กะทัดรัด — คำสั่งไม่ต้องเข้ารหัส register operand ทำให้ binary ขนาดเล็กลง
- คอมไพล์เป็น native ได้ง่าย — compiler สมัยใหม่สามารถแปลง stack operation เป็น register-based native code ได้อย่างมีประสิทธิภาพ
Stack Machine ใน WAT
หัวข้อที่มีชื่อว่า “Stack Machine ใน WAT”เมื่อคุณเขียน WAT คุณเขียนคำสั่งเหล่านั้นเรียงกันในลักษณะ stack:
(module (func (export "add") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.add))local.get $a— push ค่า parameter$aลง stacklocal.get $b— push ค่า parameter$bลง stacki32.add— pop ทั้งสอง, push ผลบวก
ค่าที่อยู่บนสุดของ stack เมื่อฟังก์ชันจบคือ return value เนื่องจากเราประกาศ (result i32) validator จะตรวจสอบว่า stack มี i32 เดียวบนสุดเมื่อฟังก์ชันจบลง
ลองรันดู
หัวข้อที่มีชื่อว่า “ลองรันดู”ลองรัน WAT ด้านล่างเพื่อเห็นว่า stack machine ทำงานอย่างไรในทางปฏิบัติ:
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Stack-based bytecode (Wasm) | encode กะทัดรัด, type checker walk stack แบบ static ได้ง่าย, คอมไพล์เป็น native เร็ว | ต้องมี compile step ก่อนรัน เพิ่ม instantiation overhead เทียบกับ JS ที่ parse แล้วรันได้เลย |
| Register-based bytecode (เช่น Dalvik) | ลดจำนวน push/pop instruction, บาง workload รันเร็วกว่าในระดับ interpreter | encode ซับซ้อนกว่า, verifier ต้อง track register state แทนที่จะ walk stack ตรงไปตรงมา |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- คิดว่า stack machine model แปลว่า Wasm รันช้ากว่า native เพราะ “ต้อง push/pop ตลอด” — จริงๆ compiler แปลง stack operation เป็น register-based machine code ก่อนรันเสมอ ไม่มี interpreter overhead แบบที่คิด
- เขียนทุกฟังก์ชันในแอปเป็น Wasm module ทั้งที่ควร profile หา hot path ที่ CPU-bound จริงๆ ก่อน แล้วค่อยย้ายเฉพาะส่วนนั้น
- มองข้าม cost ของการ compile และ instantiate module เล็กๆ ที่ถูกเรียกไม่บ่อย — ถ้า module มีขนาดเล็กและเรียกไม่กี่ครั้ง overhead ของ fetch + compile + instantiate อาจมากกว่าเวลาที่ประหยัดได้จาก stack machine execution
💡 ตัวอย่างจากของจริง
Figma เขียน rendering engine ใหม่จาก C++ เป็น Wasm ทำให้เวลาโหลดไฟล์เร็วขึ้นประมาณ 3 เท่า เพราะ Wasm module ถูก validate เป็น stack-based bytecode แล้วคอมไพล์เป็น native code ได้เกือบทันที
Google Earth พอร์ต C++ engine เดิมมารันใน browser ผ่าน Wasm โดยอาศัยโมเดล stack machine เดียวกันนี้ในการ verify และคอมไพล์ geometry/rendering code จำนวนมหาศาลให้ปลอดภัยและเร็ว