Categories
程式開發

AssemblyScript如何幫助WebAssembly發揮潛力?


WebAssembly(或Wasm)是Web瀏覽器中相對較新的功能,但它有潛力極大地擴展Web作為一個應用程序服務平台的能力。 Web開發人員在入門WebAssembly時可能會經歷艱難的學習過程,而AssemblyScript就提供了一種解決辦法。首先我們來看一下為什麼WebAssembly是一項很有前途的技術,然後再介紹AssemblyScript是怎樣幫助WebAssembly發揮潛力的。

WebAssembly

WebAssembly是針對瀏覽器使用的底層語言,為開發人員提供了JavaScript之外的Web編譯目標。它使網站代碼可以在安全的沙盒環境中以接近原生的速度運行。

它是根據所有主流瀏覽器(Chrome、Firefox、Safari和Edge)代表的意見開發的,這些代表於2017年初達成了設計共識。所有這些瀏覽器現在都支持WebAssembly,意味著整個市場中約87%的瀏覽器可以使用它。

WebAssembly以二進制格式交付,這意味著與JavaScript相比,WebAssembly在大小和加載時間上均佔優勢。但它也有供人類閱讀的文本表示形式

當WebAssembly首次亮相時,一些開發人員認為它最後有可能取代JavaScript,成為Web的主要語言。但最好將WebAssembly視為與現有Web平台集成良好的一項新工具,這也是其高階目標之一

WebAssembly並沒有取代現有的JavaScript用例,而是開拓了新的用戶場景,吸引了更多人的興趣。 WebAssembly尚不能直接訪問DOM,並且大多數現有網站都希望繼續使用JavaScript——畢竟經過多年的優化,JavaScript已經相當快了。下面是WebAssembly自身提供的可行用例列表:

  • 遊戲
  • 科學計算的可視化和模擬
  • CAD應用
  • 圖像/視頻編輯

這些用例的共同屬性是,我們通常會將它們視為桌面應用程序。 WebAssembly可以為CPU密集型任務提供接近原生平台的性能表現,這樣人們就能將更多桌面型應用程序移植到Web端了。

現有網站也可以從WebAssembly中受益。Figma提供了一個現實應用的示例,它使用WebAssembly大大縮短了加載時間。如果網站使用的某些代碼需要進行大量的計算,則可以只將這部分代碼替換為WebAssembly以提高性能。

所以也許現在你就有興趣開始使用WebAssembly了。你可以學習這種語言本身並直接編寫它,但實際上它打算成為其他語言的編譯目標。它被設計為對C和C++具有良好的支持,Go在1.11版中添加了對它的實驗性支持,Rust也對其投入了大量資源

但也許你並不想為了使用WebAssembly而學習或使用其中的任何一種語言。這就輪到AssemblyScript出場表現了。

AssemblyScript

AssemblyScript是一個TypeScript到WebAssembly的編譯器。由Microsoft開發的TypeScript為JavaScript添加了類型。它已經非常流行了,但就算用戶不怎麼熟悉TS,AssemblyScript也只支持TypeScript功能的一個有限子集,因此不需要花很長時間就能上手。

因為它與JavaScript非常相似,所以AssemblyScript使Web開發人員可以輕鬆地將WebAssembly整合到他們的網站中,而不必使用某種完全不同的語言。

嘗試一下

下面我們試著編寫第一個AssemblyScript模塊(所有代碼都在這個GitHub倉庫中提供:https://github.com/dguo/assemblyscript-demo)。為了支持WebAssembly,我們需要的Node.js最低版本是8.0

轉到一個空目錄,創建一個package.json文件,然後安裝AssemblyScript。請注意,我們需要直接從其GitHub倉庫安裝它。它尚未在npm上發布,因為AssemblyScript開發人員認為這個編譯器尚未準備好應對一般用途。

mkdir assemblyscript-demo
cd assemblyscript-demo
npm init
npm install --save-dev github:AssemblyScript/assemblyscript

使用隨附的asinit命令生成腳手架文件:

npx asinit .

我們的package.json現在應該包含以下腳本:

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"
  }
}

頂層index.js看起來像這樣:

const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
  env: {
    abort(_msg, _file, line, column) {
       console.error("abort called at index.ts:" + line + ":" + column);
    }
  }
};
Object.defineProperty(module, "exports", {
  get: () => new WebAssembly.Instance(compiled, imports).exports
});

它使我們可以輕鬆地require我們的WebAssembly模塊,就像require普通的JavaScript模塊一樣。其中,assembly目錄包含我們的AssemblyScript源代碼。生成的示例是一個簡單的加法函數。

export function add(a: i32, b: i32): i32 {
  return a + b;
}

你可能以為函數簽名會像TypeScript中的形式,也就是ad​​d(a: number, b: number): number這種格式;但這里之所以使用i32,原因是AssemblyScript使用了WebAssembly的特定整數和浮點類型,而不是TypeScript的通用數字類型。下面我們來構建示例。

npm run asbuild

現在,build目錄應包含以下文件:

optimized.wasm
optimized.wasm.map
optimized.wat
untouched.wasm
untouched.wasm.map
untouched.wat

我們得到了構建的普通版本和優化版本。對於每個構建版本,我們都有了一個.wasm二進製文件、一個.wasm.map源映射,以及該二進製文件的.wat文本表示形式。文本表示形式是用來供人類閱讀的,但在這個例子中我們無需閱讀或理解它——使用AssemblyScript的其中一個目的,就是用不著使用原始的WebAssembly了。

啟動Node,並像其他模塊一樣使用我們的編譯模塊。

$ node
Welcome to Node.js v12.10.0.
Type ".help" for more information.
> const add = require('./index').add;
undefined
> add(3, 5)
8

從Node調用WebAssembly就只需要這些步驟!

添加監視腳本

在開發時,建議你在更改源代碼時使用onchange自動重建模塊,因為AssemblyScript尚不包含監視模式

npm install --save-dev onchange

將asbuild:watch腳本添加到package.json。加入-i標誌,可在運行命令後立即運行初始構建。

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
    "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
    "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
    "asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild"
  }
}

現在你可以運行asbuild:watch,這樣就用不著不斷重新運行asbuild了。

性能

我們來寫一個基本的基準測試,看看我們可以獲得怎樣的性能提升。 WebAssembly的專長是處理諸如數字計算之類的CPU密集型任務,所以我們這裡使用一個函數來確定一個整數是否為質數。

我們的參考實現如下所示。這是一種原始的暴力解決方案,因為我們的目標是執行大量計算。

function isPrime(x) {
    if (x < 2) {
        return false;
    }

    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

等效的AssemblyScript版本僅需要一些類型註釋:

function isPrime(x: u32): bool {
    if (x < 2) {
        return false;
    }

    for (let i: u32 = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

我們將使用Benchmark.js(https://benchmarkjs.com/)。

npm install --save-dev benchmark

創建benchmark.js :

const Benchmark = require('benchmark');

const assemblyScriptIsPrime = require('./index').isPrime;

function isPrime(x) {
    for (let i = 2; i < x; i++) {
        if (x % i === 0) {
            return false;
        }
    }

    return true;
}

const suite = new Benchmark.Suite;
const startNumber = 2;
const stopNumber = 10000;

在我的機器上,運行node benchmark時得到了以下結果:

AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled)
JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled)
AssemblyScript isPrime is ~20.2% faster.

請注意,這個測試是一個microbenchmark,我們不應該太看重它的結果。

如果你想要參考一些更深度的AssemblyScript基準測試,我建議了解WasmBoy基準測試wave equation基準測試

加載模塊

接下來我們在網站中使用我們的模塊。創建index.html:



    
        
        AssemblyScript isPrime demo
    
    
        
            
            
            
        

        

創建demo.js。要加載WebAssembly模塊有多種方法,但最有效的方法是使用WebAssembly.instantiateStreaming函數,以流方式編譯和實例化這些模塊。請注意,我們需要提供一個中止函數,如果斷言失敗就會調用這個中止函數。

(async () => {
    const importObject = {
        env: {
            abort(_msg, _file, line, column) {
                console.error("abort called at index.ts:" + line + ":" + column);
            }
        }
    };
    const module = await WebAssembly.instantiateStreaming(
        fetch("build/optimized.wasm"),
        importObject
    );
    const isPrime = module.instance.exports.isPrime;

    const result = document.querySelector("#result");
    document.querySelector("#prime-checker").addEventListener("submit", event => {
        event.preventDefault();
        result.innerText = "";
        const number = event.target.elements.number.value;
        result.innerText = `${number} is ${isPrime(number) ? '' : 'not '}prime.`;
    });
})();

現在安裝static-server。我們需要一個服務器,因為使用WebAssembly.instantiateStreaming時,該模塊需要MIME類型的application/wasm。

npm install --save-dev static-server

將腳本添加到package.json。

{
  "scripts": {
    "serve-demo": "static-server"
  }
}

運行npm run serve-demo命令,並在瀏覽器中打開localhost URL。在表單中提交一個數字,你將收到一條消息,指出該數字是否為質數。到這裡,從編寫AssemblyScript,到在網站中實際使用它的整個流程我們都走了一遍。

結論

WebAssembly和它的AssemblyScript擴展並不會一夜之間加快所有網站的速度,但這也不是它們的目的。 WebAssembly之所以令人興奮,是因為它為Web開拓了更多的可能性,從而支持更多種類的應用程序。

類似地,AssemblyScript使更多開發人員可以快速上手WebAssembly,這樣我們就能在一般場景中繼續使用JavaScript,而在需要大量數字運算的任務中輕鬆切換到WebAssembly了。

原文鏈接
https://blog.logrocket.com/the-introductory-guide-to-assemblyscript/