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

การคำนวณทางคณิตศาสตร์และการเปรียบเทียบ

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 สะท้อนคำสั่งของจำนวนเต็ม แต่คำนำหน้าชนิด (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 ที่มีการลบ การหารแบบมีเครื่องหมาย และการเปรียบเทียบแบบน้อยกว่าชนิดมีเครื่องหมาย

WebAssembly
ตัวเลือกBenefitCost
เขียน 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 อย่างการหารด้วยศูนย์

`i32.lt_s` คืนค่าอะไรเมื่อการเปรียบเทียบเป็นจริง?
ความแตกต่างระหว่าง `i32.div_s` กับ `i32.div_u` คืออะไร?
คำสั่งใดที่คูณค่า i32 สองค่า?
การเปรียบเทียบของ Wasm อย่าง `i32.eq` push ชนิดใดขึ้นไปบน stack?