WebAssembly體驗之編碼base64(AssemblyScript使用教程)

萌信 發表於 2020-11-29

前言

WebAssembly 不用多說懂的都懂,將運算函式通過 c++ 等編譯為二進位制的 .wasm 檔案後,再通過 JavaScript 的 WebAssembly Api 呼叫即可進行“快速”計算。

下面快速上手體驗一把,不使用 c++ 等編譯 .wasm 檔案。

WebAssembly 中文網:WebAssembly-CN

WebAssembly 英文官網:WebAssembly-EN

使用

AssemblyScript

AssemblyScript 是 WebAssembly 社群的一個 JavaScript 解決方案,通過編寫 TypeScript 來達到近似 C 語言的型別限制,完成預編譯,進而做到編譯出二進位制 .wasm 檔案。

AssemblyScript 官方專案:AssemblyScript / assemblyscript

那他與使用 c++ 等進行編譯有什麼限制呢,主要是兩點:

  1. 型別不全面。因為是通過 ts 給出近似實現,所以目前不可以使用複雜型別(比如 RegExpTextEncoder 等),且 h5 新 api 大部分也不能使用。

  2. 不可以使用第三方依賴。

對於第一點型別限制,由於是使用 ts 編寫所以不支援的型別會報錯還是很友好的,對於第二點,意味著所有功能都需要我們純手撕,另外很多 h5 api 用不了的情況下,更增加了複雜性。

搭建環境

官方文件:AssemblyScript

先初始化專案:

	yarn init -y

安裝兩個基本依賴:

	yarn add @assemblyscript/loader
	yarn add -D assemblyscript

其中 @assemblyscript/loader 是微型載入器,可以幫我們省去很多配置,即開即用。assemblyscript 是開發核心,內建了所有可用的型別宣告(在 node_modules/assemblyscript/std 下)。

初始化專案:

	yarn asinit .

此時會列印將要生成的檔案結構,輸入 Y 確認,將得到一個基礎開發目錄:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

我們關注的只是在 assembly/index.ts 內函式編寫邏輯。

base64 邏輯實現

雖然 base64 是個小功能,但是痛點啪的一下就凸顯出來了,很快啊。

我們不能使用第三方庫,於是乎 js-base64 是不能用的。去把原始碼搬進來行不行?是不行的,因為 js-base64 使用了很多 h5 的 api 與 RegExp 正則替換,所以我們只能實現一個 乞丐版 的:編碼 base64:

// ./assembly/index.ts
export class Base64 {

    _keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    encode(input: string): string {
        var output = "";
        var chr1: i32, chr2: i32, chr3: i32, enc1: i32, enc2: i32, enc3: i32, enc4: i32;
        var i = 0;

        input = this._utf8_encode(input);

        while (i < input.length) {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);

            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;

            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            }
            else if (isNaN(chr3)) {
                enc4 = 64;
            }

            output = output +
                this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        } // Whend 

        return output;
    } // End Function encode 

    _utf8_encode(string: string): string {
        var utftext = "";

        for (var n = 0; n < string.length; n++) {
            var c = string.charCodeAt(n);

            if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if ((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }

        } // Next n 

        return utftext;
    }

}

export function getBase64(): Base64 {
    return new Base64()
} 

語法規則

編寫 AssemblyScript 就要遵守他的規則,我們著重需要確定的是三個部分:

  1. 迴歸原始。不使用 h5 新 api ( atobTextEncoder 等),不接觸複雜型別( RegExp 等),如果編輯器紅線報錯就是找不到相對應的型別了,說明這個型別無法使用。

  2. 注意數字型別。對於 number 型別,AssemblyScript 含有更具體的 i32i64f32 等型別(見官方文件 Types ),在相應的變數初始化時對其指定合適的型別。

  3. 明確基本型別。變數初始化要指明其基礎型別,以便 AssemblyScript 預編譯。

到此為止,我們完成了最核心的 AssemblyScript 邏輯編寫。

此處需要我們注意兩個問題:

  1. 上面這個邏輯是簡單版的,比如換行等邊界情況是沒有考慮的。

  2. 為什麼要寫成單例模式,這麼寫是官方推薦的匯出類寫法,採用相同的引用節省資源。

構建 wasm

執行打包:

	yarn asbuild

之後就會在 ./build 下得到優化後和優化前的 .wasm 二進位制檔案:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

實際呼叫

./test/index.js 內編寫:

const loader = require("@assemblyscript/loader"); 

const fs =require('fs')

const text = '年輕人不講武德'

loader.instantiate(
    fs.readFileSync('../build/optimized.wasm'),
  {env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
).then(({ exports }) => {

  console.time("測試 wasm 速度: ")

  const { __getString, __retain, __newString, __release } = exports
  const { getBase64, Base64 } = exports
  const Base64Ptr = getBase64()
  const base64 = Base64.wrap(Base64Ptr)

  for(let i = 0;i<10000;i++) {
    const textPtr = __retain(__newString(text))
    const outputPtr = base64.encode(textPtr)
    // console.log(__getString(outputPtr))
    __release(textPtr)
    __release(outputPtr)
  }

  __release(Base64Ptr)

  console.timeEnd("測試 wasm 速度: ")

})

下面我們分塊講解:

loader.instantiate(
    fs.readFileSync('../build/optimized.wasm'),
    {env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
)

作用:初始化一個微型載入器 loader ,他內建了很多預設,可以幫我們省去很多配置的功夫,往往我們只需要指定 env.memory 引數定義初始記憶體值即可(在這裡不指定也可),當你需要更大記憶體運算時,記得指定。

引數1:這裡第一個引數是讀入 .wasm 檔案,可以是 fs 本地讀取,也可以是 fetch 遠端拉取(在瀏覽器的情況)。

引數2:loader 的配置,正常情況可以不傳,採用預設配置,更多配置詳見 node_modules/@assemblyscript/loader/index.d.ts 中的 Imports 與官網說明。

.then(({ exports }) => {

  console.time("測試 wasm 速度: ")
  // ...
  console.timeEnd("測試 wasm 速度: ")

})

拿到非同步載入的結果並結構得到 exports ,即為我們在 ./assembly/index.ts 匯出的函式。

注:實際上 exports 並不只有我們匯出的函式,他還有一些“輔助”函式,幫助我們更友好的使用 WebAssembly 。

  // 匯出輔助函式
  const { __getString, __retain, __newString, __release } = exports
  // 匯出我們編寫的函式
  const { getBase64, Base64 } = exports
  // 獲取 class 單例的指標
  const Base64Ptr = getBase64()
  // 將指標轉為實際的 class 類
  const base64 = Base64.wrap(Base64Ptr)

在這裡,我們先將輔助函式匯出,為什麼需要輔助函式?因為在 WebAssembly 中,不存在字串和 class 等基本型別的概念,一切均為 buffer 與 number ,所以輔助函式的作用就是幫我們做了 string 和 class 的中間轉化處理,真是非常友好了!

namedescription
__getString從一個 string 的指標獲取實際的字串值
__retain定義一個引用 id ,以便後續回收,多次呼叫的 id 會進行累加(並不需要我們維護)
__newString將實際字串轉為 string 的指標,以便傳入
__release根據 __retain 釋放引用

更多輔助函式請看官網說明:loader usage

  // 執行 10000 次編碼
  for(let i = 0;i<10000;i++) {
  	// 獲取一個 string 的指標
    const textPtr = __retain(__newString(text))
    // 傳入指標得到返回值 string 的指標
    const outputPtr = base64.encode(textPtr)
    
    // 可以通過 __getString 方法將 string 指標轉為實際的字串
    // console.log(__getString(outputPtr))
    
    // 清理引用
    __release(textPtr)
    __release(outputPtr)
  }
  
  __release(Base64Ptr)

我們對其做 10000 次編碼,實際中被編碼的 text 會發生變化,可能時間會更長:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

校驗

我們列印一次出來看一下結果:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

校驗:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

成功!

對比

下面我們用相同的程式碼進行測試直接使用的速度:

const { getBase64 } = require('./encode')

console.time("測試直接使用速度")

const base = getBase64()

for(let i = 0;i<10000;i++) {
  base.encode(text)
  // console.log(base.encode(text))
}

console.timeEnd("測試直接使用速度")

結果:

WebAssembly體驗之編碼base64(AssemblyScript使用教程)

總結

目前 WebAssembly 主要是應用在音視訊處理和網頁遊戲上,可以藉助相關庫的便利性,比如 ffmpeg 實現轉碼,音視訊壓縮,B 站視訊上傳過程即可選擇封面等。

加上規範的不成熟,舊版本瀏覽器相容問題,以及如此緩慢的速度,雖然不能否定,但也請不要太吹噓 WebAssembly 了。