การคำนวณทางคณิตศาสตร์และการเปรียบเทียบ
WebAssembly มีชุดคำสั่งทางคณิตศาสตร์และการเปรียบเทียบที่หลากหลาย แต่ละคำสั่งมีชนิด (typed) — คุณรู้เสมอว่ากำลังทำงานกับจำนวนเต็มหรือ float และสำหรับจำนวนเต็ม operation นั้นเป็นแบบมีเครื่องหมายหรือไม่มีเครื่องหมาย
การคำนวณกับจำนวนเต็ม
หัวข้อที่มีชื่อว่า “การคำนวณกับจำนวนเต็ม”Operation พื้นฐานสี่อย่างสำหรับ i32 คือ i32.add, i32.sub, i32.mul และการหาร ทั้งหมด pop ตัวถูกดำเนินการสองตัวและ push ผลลัพธ์หนึ่งตัว:
(module (func (export "add") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.add) (func (export "sub") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.sub) (func (export "mul") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.mul))คำสั่งเดียวกันนี้มีอยู่สำหรับ i64 เช่นกัน: i64.add, i64.sub, i64.mul
การหารแบบมีเครื่องหมายเทียบกับไม่มีเครื่องหมาย
หัวข้อที่มีชื่อว่า “การหารแบบมีเครื่องหมายเทียบกับไม่มีเครื่องหมาย”การหารเป็นจุดที่ชนิดจำนวนเต็มเผยให้เห็นความซับซ้อนของตัวเอง i32 ก็แค่ 32 บิต — ตัวบิตเองไม่ได้พกพาข้อมูลเครื่องหมายใด ๆ คำสั่ง เป็นตัวตัดสินว่าจะตีความบิตเหล่านั้นเป็นจำนวนเต็มแบบมีเครื่องหมายในรูป two’s-complement หรือเป็นจำนวนเต็มแบบไม่มีเครื่องหมาย:
(module ;; signed: -7 / 2 = -3 (rounds toward zero) (func (export "divSigned") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.div_s)
;; unsigned: the same bit pattern as -7 (0xFFFFFFF9) / 2 = 2147483644 (func (export "divUnsigned") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.div_u))ในทำนองเดียวกัน i32.rem_s และ i32.rem_u ให้เศษเหลือแบบมีเครื่องหมายและไม่มีเครื่องหมาย
การคำนวณกับ float
หัวข้อที่มีชื่อว่า “การคำนวณกับ float”คำสั่งสำหรับ float สะท้อนคำสั่งของจำนวนเต็ม แต่คำนำหน้าชนิด (type prefix) เปลี่ยนไป:
(module (func (export "fadd") (param $a f64) (param $b f64) (result f64) local.get $a local.get $b f64.add))f64.sub, f64.mul, f64.div, f64.sqrt, f64.floor, f64.ceil, f64.min และ f64.max มีให้ใช้ทั้งหมด ชุดเดียวกันนี้มีอยู่สำหรับ f32 เช่นกัน
คำสั่งเปรียบเทียบ
หัวข้อที่มีชื่อว่า “คำสั่งเปรียบเทียบ”การเปรียบเทียบจะ pop ค่าสองค่าและ push ผลลัพธ์ i32: 1 หมายถึง true, 0 หมายถึง false ไม่มีชนิดบูลีน — WebAssembly ใช้ i32 เป็นบูลีนเมื่อใดก็ตามที่ต้องการ
(module ;; returns 1 if $a equals $b, else 0 (func (export "eq") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.eq)
;; signed less-than: returns 1 if $a < $b (signed), else 0 (func (export "ltSigned") (param $a i32) (param $b i32) (result i32) local.get $a local.get $b i32.lt_s))ตัวแปรของการเปรียบเทียบเป็นไปตามรูปแบบ signed/unsigned เดียวกัน: i32.lt_s เทียบกับ i32.lt_u, i32.gt_s เทียบกับ i32.gt_u, i32.le_s, i32.ge_u และอื่น ๆ
ลองรันดู
หัวข้อที่มีชื่อว่า “ลองรันดู”ตัวอย่างด้านล่างคอมไพล์ module ที่มีการลบ การหารแบบมีเครื่องหมาย และการเปรียบเทียบแบบน้อยกว่าชนิดมีเครื่องหมาย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เขียน WAT เอง | เห็น operand order และ opcode ตรง ๆ ทำให้เข้าใจว่า i32.div_s กับ i32.div_u ต่างกันตรงไหนแบบไม่มีอะไรบัง | ต้องนับ stack เองทุกบรรทัด ผิดง่ายเวลาโค้ดยาวขึ้น |
| ปล่อยให้ compiler สร้าง Wasm (เช่นจาก Rust/C) | เขียนโค้ดระดับสูงตามปกติ ได้ output ที่ optimize แล้ว เหมาะกับงานจริง | มองไม่เห็นว่า compiler เลือก _s หรือ _u variant ตรงไหน debug ยากกว่าถ้าผลลัพธ์ผิด |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- สลับลำดับ operand ตอน push ขึ้น stack เช่นตั้งใจจะทำ
a - bแต่ push$bก่อน$aทำให้i32.subคำนวณb - aแทน เพราะ WAT เป็น stack-based ลำดับการ push มีผลโดยตรงต่อผลลัพธ์ - ใช้
div_s/div_uหรือlt_s/lt_uสลับกัน เพราะi32ไม่มีเครื่องหมายในตัวเอง ถ้าเลือกคำสั่งผิด ผลลัพธ์จะผิดแบบเงียบ ๆ ไม่มี error ให้เห็น - ลืมว่าค่าที่เหลือบน stack ตอนจบฟังก์ชันต้องตรงกับ
resultที่ประกาศไว้พอดี ถ้าคำนวณแล้วเหลือค่าเกินหรือขาดไป module จะ validation ไม่ผ่าน
💡 ตัวอย่างจากของจริง
ทีม WABT (WebAssembly Binary Toolkit) ใช้ไฟล์
.watที่เขียนมือจำนวนมากในชุด test spec compliance เพื่อยืนยันว่า opcode อย่างdiv_s/div_uทำงานตรงตาม spec ทุกกรณี รวมถึง edge case อย่างการหารด้วยศูนย์