數值運算


要熟悉基本的堆疊操作,可從數值運算開始,目前經常看到的是將一個常數推入堆疊,也就是在 i32i64f32f64 之後,接下 .const

  • i32.const
  • i64.const
  • f32.const
  • f64.const

需要在堆疊中放入幾個數值,視接下來要進行的運算而定,底下的指令運算結果會置入堆疊。

整數運算

在整數運算方面,i32i64 擁有相同的指令集,例如 i32.add 就有對應的 i64.add,常見的加減乘除等是二元運算,以 i32 為例,二元運算中與常見數學運算相關的指令有:

  • i32.add:相加
  • i32.sub:相減
  • i32.mul:相乘
  • i32.div_s:有號相除,捨去小數
  • i32.div_u:無號相除,採用 floor 捨入
  • i32.rem_s:有號餘除(結果採被除數之正負)
  • i32.rem_u:無號餘除

與位元運算相關的指令有:

  • i32.and:AND 位元運算
  • i32.or:OR 位元運算
  • i32.xor:XOR 位元運算
  • i32.shl:位元左移
  • i32.shr_u:補 0 位元右移
  • i32.shr_s:補最左位元之位元右移
  • i32.rotl:循環位元左移
  • i32.rotr:循環位元右移

與比較運算相關的指令有底下,在 WebAssembly 中,成立都是輸出 1,不成立為 0:

  • i32.eq:相等
  • i32.ne:不相等
  • i32.lt_s:有號小於
  • i32.le_s:有號小於等於
  • i32.lt_u:無號小於
  • i32.le_u:有號小於等於
  • i32.gt_s:有號大於
  • i32.ge_s:有號大於等於
  • i32.gt_u:無號大於
  • i32.ge_u:無號大於等於

因為是二元運算,堆疊中必須先置入兩個數值,再來呼叫以上的指令,例如 1 + 2:

(module
    (import "env" "log" (func $log (param i32)))
    (func $main
        i32.const 1
        i32.const 2
        i32.add
        call $log ;; 在 conole 顯示 3
    )
    (start $main)
)

在這邊看到註解的另一個形式,也就是在 ;; 之後加上註解文字,這在編譯時會被忽略掉。

實際上,如果已經熟悉堆疊操作上的順序,可以寫為:

(module
    (import "env" "log" (func $log (param i32)))
    (func $main
        (i32.add (i32.const 1) (i32.const 2))
        call $log
    )
    (start $main)
)

括號內的會先執行,可以回顧一下〈從 C 到 WebAssembly〉中,轉譯出來的 Wat,就有這類的寫法,進一步地觀察它,你就可以寫出:

(module
    (import "env" "log" (func $log (param i32)))
    (func $main
        (call $log
            (i32.add (i32.const 1) (i32.const 2))
        )
    )
    (start $main)
)

簡單來說,先習慣堆疊操作邏輯,有機會也多觀察一下 C 轉譯出的的 Wat 長什麼樣,就會寫出更自然的程式碼了,基於這一系列文件是入門的基礎,之後多半會使用較基本的寫法,若對可讀性有很大幫助,才會使用如上較自然的寫法。

如果是一元運算,只要在堆疊中置入一個數值,就可以呼叫以下指令:

  • i32.clz:左邊有幾個 0 的位元
  • i32.ctz:右邊有幾個 0 的位元
  • i32.popcn:有幾個 1 的位元
  • i32.eqz:是否為 0

浮點數運算

在浮點數方面,f32f64 擁有相同的指令集,以 f32 為例,其中二元運算的指令有:

  • f32.add:相加
  • f32.sub:相減
  • f32.mul:相乘
  • f32.div:相除
  • f32.copysign:令左運算元(先置入堆疊的值)正負號與右運算元相同
  • f32.eq:相等,nan 視為不等於 nan
  • f32.ne:不相等,nan 視為不等於 nan
  • f32.lt:小於,nan 視為不等於 nan
  • f32.le:小於等於,nan 視為不等於 nan
  • f32.gt:大於,nan 視為不等於 nan
  • f32.ge:大於等於,nan 視為不等於 nan
  • f32.min:最小值,若運算元之一為 nan,傳回 nan
  • f32.max:最大值,若運算元之一為 nan,傳回 nan

一元運算的指令有:

  • f32.abs:絕對值
  • f32.neg:改變正負號
  • f32.ceil:ceil 捨入
  • f32.floor:floor 捨入
  • f32.trunc:round 至最接近 0 的整數
  • f32.nearest:round 至最接近的偶數
  • f32.sqrt:平方根

(有關於 round、ceil、round,可參考〈算錢學問大〉。)

型別轉換運算

不同型態的數值,會有放在一起運算的機會,這時必須進行適當的型態轉換,在整數部份,i32 擁有底下的型態轉換指令:

  • i32.wrap/i64:將 64 位元整數轉 32 位元整數,多的部份捨去
  • i32.trunc_s/f32:將 32 位元浮點數轉為有號 32 位元整數
  • i32.trunc_s/f64:將 64 位元浮點數轉為有號 32 位元整數
  • i32.trunc_u/f32:將 32 位元浮點數轉為無號 32 位元整數
  • i32.trunc_u/f64:將 64 位元浮點數轉為無號 32 位元整數
  • i32.reinterpret/f32:將 32 位元浮點數的位元組重新詮釋為 32 位元整數

一個例子如下,拿掉 i32.wrap/i64 的話,i32.add 會因型態不符而出錯:

(module
    (import "env" "log" (func $log (param i32)))
    (func $main
        i32.const 1
        i64.const 2
        i32.wrap/i64
        i32.add
        call $log
    )
    (start $main)
)

i64 擁有底下的型態轉換指令:

  • i64.extend_s/i32:將 32 位元整數擴充為 64 位元有號整數
  • i64.extend_u/i32:將 32 位元整數擴充為 64 位元無號整數
  • i64.trunc_s/f32:將 32 位元浮點數轉為有號 64 位元整數
  • i64.trunc_s/f64:將 64 位元浮點數轉為有號 64 位元整數
  • i64.trunc_u/f32:將 32 位元浮點數轉為無號 64 位元整數
  • i64.trunc_u/f64:將 64 位元浮點數轉為無號 64 位元整數
  • i64.reinterpret/f64:將 64 位元浮點數的位元組重新詮釋為 64 位元整數

f32 擁有底下的型態轉換指令:

  • f32.demote/f64:將 64 位元浮點數降為 32 位元浮點數
  • f32.convert_s/i32:將 32 位元有號整數轉為 32 位元浮點數
  • f32.convert_s/i64:將 64 位元有號整數轉為 32 位元浮點數
  • f32.convert_u/i32:將 32 位元無號整數轉為 32 位元浮點數
  • f32.convert_u/i64:將 64 位元無號整數轉為 32 位元浮點數
  • f32.reinterpret/i32:將 32 位元整數的位元組重新詮釋為 32 位元浮點數

f64 擁有底下的型態轉換指令:

  • f64.promote/f32:將 32 位元浮點數升為 64 位元浮點數
  • f64.convert_s/i32:將 32 位元有號整數轉為 64 位元浮點數
  • f64.convert_s/i64:將 64 位元有號整數轉為 64 位元浮點數
  • f64.convert_u/i32:將 32 位元無號整數轉為 64 位元浮點數
  • f64.convert_u/i64:將 64 位元無號整數轉為 64 位元浮點數
  • f64.reinterpret/i64:將 64 位元整數的位元組重新詮釋為 64 位元浮點數