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

โมเดล Stack Machine

WebAssembly ทำงานบนหลักการที่เรียบง่ายแต่ทรงพลัง: stack machine แทนที่จะใช้ register ที่มีชื่อเหมือน CPU ทั่วไป Wasm จะ push และ pop ค่าบน operand stack ที่ implicit เพื่อรันคำสั่งทุกตัว

ใน 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"]
การทำงานของ Stack Machine
  1. i32.const 3 — push ค่า 3 ลงบน stack
  2. i32.const 4 — push ค่า 4 ลงบน stack
  3. i32.add — pop ค่า 2 ตัว (3 และ 4), บวกเข้าด้วยกัน, push ผลลัพธ์ (7) กลับ
  4. ฟังก์ชันจบลง ค่า 7 บนสุดของ stack กลายเป็น return value

Stack machine มีข้อได้เปรียบหลายประการสำหรับ portable bytecode:

  • ตรวจสอบได้ง่าย — type checker สามารถ walk stack statically และตรวจสอบความถูกต้องของ type ได้โดยไม่ต้องวิเคราะห์ที่ซับซ้อน
  • กะทัดรัด — คำสั่งไม่ต้องเข้ารหัส register operand ทำให้ binary ขนาดเล็กลง
  • คอมไพล์เป็น native ได้ง่าย — compiler สมัยใหม่สามารถแปลง stack operation เป็น register-based native code ได้อย่างมีประสิทธิภาพ

เมื่อคุณเขียน 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 ลง stack
  • local.get $b — push ค่า parameter $b ลง stack
  • i32.add — pop ทั้งสอง, push ผลบวก

ค่าที่อยู่บนสุดของ stack เมื่อฟังก์ชันจบคือ return value เนื่องจากเราประกาศ (result i32) validator จะตรวจสอบว่า stack มี i32 เดียวบนสุดเมื่อฟังก์ชันจบลง

ลองรัน WAT ด้านล่างเพื่อเห็นว่า stack machine ทำงานอย่างไรในทางปฏิบัติ:

WebAssembly
ตัวเลือกBenefitCost
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 รันเร็วกว่าในระดับ interpreterencode ซับซ้อนกว่า, 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 จำนวนมหาศาลให้ปลอดภัยและเร็ว

คำสั่ง `i32.add` ทำอะไรบน Wasm operand stack?
หลังจากรัน `i32.const 3` แล้วตามด้วย `i32.const 4` มีค่ากี่ตัวบน operand stack?
เกิดอะไรขึ้นถ้า type บน stack ไม่ตรงกับ instruction signature?