藉助 WASM 進行密集計算:入門篇
在《使用 Docker 和 Golang 快速上手 WebAssembly》一文中,我介紹瞭如何製作符合 WASI 接口標準的通用 WASM,以及如何在幾種不同的場景下進行程序調用。
本篇文章將延續前文,聊聊在如何藉助 WASM 增強 Golang、Node.js ,進行更高效的密集計算。
寫在前面
或許有讀者會說,既然使用 Golang 又何必使用 Node.js,差不多的代碼實現場景,兩者性能根本不在一個數量級,比較起來不夠客觀。但其實,完全沒有必要做這樣的語言對立,除了兩種語言的絕對的性能區別之外,或許我們也應該關注下面的事情:
在相同的計算場景下,如果分別以兩種語言自身的實現方式爲基準,配合 WASM 進行業務實現,是否有可能在:程序執行效率、程序分發內容安全、插件生態上產生更好的變化呢?
以及,我們可以腦洞更大一些,兩種語言是否可以通過 WASM 的方式進行生態整合,讓豐富的 Node / NPM 生態,靈活的 JavaScript 應用和嚴謹高效的 Golang 產生化學反應呢?
在開始本文之前,我先來說幾個讓人意外的觀察結論(具體數據可以參考文末表格):
-
如果說 Go 在使用 WASM 的模式下,普遍執行速度並不會比 “原生” 差多少,甚至在個別場景下,執行速度還比原生快,你是否會感到詫異?
-
如果說原本 Node 多進程 / 線程的執行速度落後相同功能的 Golang 程序 200%,在不進行極端優化的場景下,僅僅是通過引入 WASM,就能讓差距縮小到 100%。
-
甚至在一些情況下,你會發現 Node + WASM 的性能和 Go 最佳的表現其實差距可以從 300% 縮短到接近 30%。
如果你覺得意外,但是又好奇,不妨跟隨文章自己親自來試一試。
測試環境中的幾種程序,爲了模擬最糟糕的情況,均採用 CLI 一次性觸發執行的方式來收集結果,減少 Go 語言運行時的 GC 優化、或者 Node 在運行過程中的 JIT 優化,以及字節碼緩存帶來的有益幫助。
前置準備
在開始折騰之旅之前,我們首先需要準備環境。爲了保障程序運行和測試的結果儘量客觀,我們統一使用同一臺機器,並且準備相同的容器環境,並將容器可以使用的 CPU 資源數量限制小一些,爲宿主機預留一些資源,確保測試的機器不會出現 CPU 不足的狀況。
關於構建程序使用的方式和容器環境,可以參考前一篇文章的 “環境準備”,爲了節約篇幅,就不多贅述了。
下面是各種依賴和組件的版本。
# go version
go version go1.17.3 linux/amd64
# node --version
v17.1.0
# wasmer --version
wasmer 2.0.0
#tinygo version
tinygo version 0.21.0 linux/amd64 (using go version go1.17.3 and LLVM version 11.0.0)
準備 WASM 程序
爲了更好的模擬和進行結果對比,我們需要一個計算時間比較久的 “活兒”,“斐波那契數列” 就是一個不錯的選擇,尤其是沒有進行動態規劃優化的“基礎版”。
約定模擬密集計算的場景
這裏爲了方便程序性能對比,我們統一讓程序針對斐波那契數列的第 40 位(大概 1 億多的一個整數)進行快速的重複計算。
爲了保證容器內的 CPU 充分使用,結果相對客觀,程序一律使用 “並行計算” 的方式來進行數值計算。
使用 Go 編寫 具備 WASI 標準接口的 WASM 程序
如果將上面的需求進行翻譯,僅實現斐波那契數列的計算。那麼使用 Go 不做任何算法優化的話,純計算函數的實現,代碼大概會類似下面:
package main
func main() {}
//export fibonacci
func fibonacci(n uint) uint {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return fibonacci(n-1) + fibonacci(n-2)
}
}
將上面的內容保存爲 wasm.go
,參考上篇文章中提到的編寫通用的 WASI 程序,執行 tinygo build --no-debug -o module.wasm -wasm-abi=generic -target=wasi wasm.go
。
我們將會順利的得到一個編譯好的、通用的 WASM 程序,用於稍後的程序測試中。
編寫 Go 語言基準版程序
雖然我們已經得到了 WASM 程序,但是爲了直觀的比較,我們還是需要一個完全使用 Go 實現基礎版的程序:
package main
import (
"fmt"
"time"
)
func fibonacci(n uint) uint {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return fibonacci(n-1) + fibonacci(n-2)
}
}
func main() {
start := time.Now()
n := uint(40)
fmt.Println(fibonacci(n))
fmt.Printf("🎉 都搞定了,用時:%v \n", time.Since(start).Milliseconds())
}
將上面的代碼保存爲 base.go
,然後執行 go run base.go
,將會看到類似下面的結果:
102334155
🎉 都搞定了,用時:574
如果你想追求絕對的性能,可以進行 go build base.go
,然後執行 ./base
,不過因爲代碼實在是太簡單了,從輸出結果來看,性能差異並不大。
基礎計算功能搞定後,我們來簡單調整代碼,讓代碼具備並行計算的能力:
package main
import (
"fmt"
"os"
"runtime"
"strconv"
"time"
)
func fibonacci(n uint) uint {
if n == 0 {
return 0
} else if n == 1 {
return 1
} else {
return fibonacci(n-1) + fibonacci(n-2)
}
}
func calc(n uint, ch chan string) {
ret := fibonacci(n)
msg := strconv.FormatUint(uint64(ret), 10)
fmt.Println(fmt.Sprintf("📦 收到結果 %s", msg))
ch <- msg
}
func main() {
numCPUs := runtime.NumCPU()
n := uint(40)
ch := make(chan string, numCPUs)
fmt.Printf("🚀 主進程上線 #ID %v\n", os.Getpid())
start := time.Now()
for i := 0; i < numCPUs; i++ {
fmt.Printf("👷 分發計算任務 #ID %v\n", i)
go calc(n, ch)
}
for i := 0; i < numCPUs; i++ {
<-ch
}
fmt.Printf("🎉 都搞定了,用時:%v \n", time.Since(start).Milliseconds())
}
將代碼保存爲 full.go
,然後執行 go run full.go
,將會看到類似下面的結果:
🚀 主進程上線 #ID 2248
👷 分發計算任務 #ID 0
👷 分發計算任務 #ID 1
👷 分發計算任務 #ID 2
👷 分發計算任務 #ID 3
👷 分發計算任務 #ID 4
👷 分發計算任務 #ID 5
👷 分發計算任務 #ID 6
👷 分發計算任務 #ID 7
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:658
是不是有一絲性價比的味道,前面一次計算結果接近 600 毫秒,這次併發 8 個(受限於 Docker 環境資源限制)計算,也就 650 毫秒。
編寫 Go 語言調用 WASM 程序
package main
import (
"fmt"
"os"
"runtime"
"time"
"io/ioutil"
wasmer "github.com/wasmerio/wasmer-go/wasmer"
)
func main() {
wasmBytes, _ := ioutil.ReadFile("module.wasm")
store := wasmer.NewStore(wasmer.NewEngine())
module, _ := wasmer.NewModule(store, wasmBytes)
wasiEnv, _ := wasmer.NewWasiStateBuilder("wasi-program").Finalize()
GenerateImportObject, err := wasiEnv.GenerateImportObject(store, module)
check(err)
instance, err := wasmer.NewInstance(module, GenerateImportObject)
check(err)
wasmInitial, err := instance.Exports.GetWasiStartFunction()
check(err)
wasmInitial()
fibonacci, err := instance.Exports.GetFunction("fibonacci")
check(err)
numCPUs := runtime.NumCPU()
ch := make(chan string, numCPUs)
fmt.Printf("🚀 主進程上線 #ID %v\n", os.Getpid())
start := time.Now()
for i := 0; i < numCPUs; i++ {
fmt.Printf("👷 分發計算任務 #ID %v\n", i)
calc := func(n uint, ch chan string) {
ret, _ := fibonacci(n)
msg := fmt.Sprintf("%d", ret)
fmt.Println(fmt.Sprintf("📦 收到結果 %s", msg))
ch <- msg
}
go calc(40, ch)
}
for i := 0; i < numCPUs; i++ {
<-ch
}
fmt.Printf("🎉 都搞定了,用時:%v \n", time.Since(start).Milliseconds())
}
func check(e error) {
if e != nil {
panic(e)
}
}
將代碼保存爲 wasi.go
,執行 go run wasi.go
,會得到類似下面的結果:
🚀 主進程上線 #ID 3198
👷 分發計算任務 #ID 0
👷 分發計算任務 #ID 1
👷 分發計算任務 #ID 2
👷 分發計算任務 #ID 3
👷 分發計算任務 #ID 4
👷 分發計算任務 #ID 5
👷 分發計算任務 #ID 6
👷 分發計算任務 #ID 7
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:595
多執行幾次,你會發現相比較完全使用 Go 實現的程序,多數執行結果居然會更快一些,極限的情況,也不過是差不多快。是不是性價比的味道又來了。(不過爲了保證客觀,文末會附帶多次計算結果)
使用 Node 編寫基準版程序(Cluster 模式)
相同的代碼如果使用 Node.js 來實現,行數會稍微多一點。
雖然程序文件名叫啥都行,但是爲了能在 CLI
執行上偷懶,就管它叫 index.js
吧:
const cluster = require('cluster');
const { cpus } = require('os');
const { exit, pid } = require('process');
function fibonacci(num) {
if (num === 0) {
return 0;
} else if (num === 1) {
return 1;
} else {
return fibonacci(num - 1) + fibonacci(num - 2);
}
}
const numCPUs = cpus().length;
if (cluster.isPrimary) {
let dataStore = [];
const readyChecker = () => {
if (dataStore.length === numCPUs) {
console.log(`🎉 都搞定了,用時:${new Date - start}`);
exit(0);
}
}
console.log(`🚀 主進程上線 #ID ${pid}`);
const start = new Date();
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('online', (worker) => {
console.log(`👷 分發計算任務 #ID ${worker.id}`);
worker.send(40);
});
const messageHandler = function (msg) {
console.log("📦 收到結果", msg.ret)
dataStore.push(msg.ret);
readyChecker()
}
for (const id in cluster.workers) {
cluster.workers[id].on('message', messageHandler);
}
} else {
process.on('message', (msg) => {
process.send({ ret: fibonacci(msg) });
});
}
保存好文件,執行 node .
,程序輸出內容會類似下面這樣:
🚀 主進程上線 #ID 2038
👷 分發計算任務 #ID 1
👷 分發計算任務 #ID 2
👷 分發計算任務 #ID 3
👷 分發計算任務 #ID 4
👷 分發計算任務 #ID 7
👷 分發計算任務 #ID 5
👷 分發計算任務 #ID 6
👷 分發計算任務 #ID 8
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:1747
目前來看,執行時間差不多是 Go 版程序的 3 倍,不過往好處想,可提升空間應該也非常大。
使用 Node 編寫基準版程序(Worker Threads)
當然,每當提到 Node.js Cluster
的時候,就不免會有人說 Cluster
數據交換效率低、比較笨重,所以我們同樣測試一把 Worker Threads
。
其實從 Node.js 12.x LTS 開始,Node 就具備了 Worker Threads 的能力。關於如何使用 Node.js 的 Worker Treads
,以及循序漸進的理解如何使用 SharedArrayBuffer
,可以參考這篇文章《How to work with worker threads in NodeJS》,本文就不做基礎展開了。考慮到我們模擬的是極限糟糕的情況,所以這裏也不必使用 node-worker-threads-pool
之類的三方庫,對 threads 做 pool 化了。(實測幾乎沒差別)
將上面 Cluster 版本的代碼簡單調整,我們可以得到類似下面的代碼,不同的是,爲了書寫簡單,我們這次需要拆分爲兩個文件:
const { Worker } = require("worker_threads");
const { cpus } = require('os');
const { exit } = require('process');
let dataStore = [];
const readyChecker = () => {
if (dataStore.length === numCPUs) {
console.log(`🎉 都搞定了,用時:${new Date - start}`)
exit(0);
}
}
const num = [40];
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * num.length);
const sharedArray = new Int32Array(sharedBuffer);
Atomics.store(sharedArray, 0, num);
const numCPUs = cpus().length;
console.log(`🚀 主進程上線 #ID ${process.pid}`);
const start = new Date();
for (let i = 0; i < numCPUs; i++) {
const worker = new Worker("./worker.js");
worker.on("message", msg => {
console.log("📦 收到結果", msg.ret)
dataStore.push(msg.ret);
readyChecker()
});
console.log(`👷 分發計算任務`);
worker.postMessage({ num: sharedArray });
}
可以考慮換個目錄,先將上面的內容同樣保存爲 index.js
。我們繼續來完成 worker.js
的內容。
const { parentPort } = require("worker_threads");
function fibonacci(num) {
if (num === 0) {
return 0;
} else if (num === 1) {
return 1;
} else {
return fibonacci(num - 1) + fibonacci(num - 2);
}
}
parentPort.on("message", data => {
parentPort.postMessage({ num: data.num, ret: fibonacci(data.num) });
});
在文件都保存就緒後,執行 node .
同樣可以看到類似下面的輸出:
🚀 主進程上線 #ID 2190
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:1768
這裏應該是因爲交換的數據量比較少,所以執行時間其實和 Cluster
版本差不多。
使用 Node 調用 WASM 程序(Cluster)
簡單調整上文中的 Cluster 模式的代碼,來實現一個能夠使用 WASM 進行計算的程序:
const { readFileSync } = require('fs');
const { WASI } = require('wasi');
const { argv, env } = require('process');
const cluster = require('cluster');
const { cpus } = require('os');
const { exit, pid } = require('process');
const numCPUs = cpus().length;
if (cluster.isPrimary) {
let dataStore = [];
const readyChecker = () => {
if (dataStore.length === numCPUs) {
console.log(`🎉 都搞定了,用時:${new Date - start}`);
exit(0);
}
}
console.log(`🚀 主進程上線 #ID ${pid}`);
const start = new Date();
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('online', (worker) => {
console.log(`👷 分發計算任務 #ID ${worker.id}`);
worker.send(40);
});
const messageHandler = function (msg) {
console.log("📦 收到結果", msg.ret)
dataStore.push(msg.ret);
readyChecker()
}
for (const id in cluster.workers) {
cluster.workers[id].on('message', messageHandler);
}
} else {
process.on('message', async (msg) => {
const wasi = new WASI({ args: argv, env });
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
const wasm = await WebAssembly.compile(readFileSync("./module.wasm"));
const instance = await WebAssembly.instantiate(wasm, importObject);
wasi.start(instance);
const ret = await instance.exports.fibonacci(msg)
process.send({ ret });
});
}
將內容保存爲 index.js
後,執行 node --experimental-wasi-unstable-preview1 .
後,會看到類似下面的輸出結果:
🚀 主進程上線 #ID 2338
(node:2338) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
👷 分發計算任務 #ID 1
👷 分發計算任務 #ID 3
👷 分發計算任務 #ID 2
👷 分發計算任務 #ID 5
👷 分發計算任務 #ID 6
👷 分發計算任務 #ID 8
👷 分發計算任務 #ID 4
👷 分發計算任務 #ID 7
(node:2345) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2346) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2360) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2350) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2365) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2377) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2371) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2354) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:808
是不是再次嗅到了一絲真香的味道。在幾乎不需要怎麼費勁的情況下,簡單調整下代碼,執行效率比不使用 WASM 少了一半的時間,甚至在降低了一個數量級之後,如果繼續優化、以及讓程序持續的運行,或許甚至能夠無限趨近於 Go 版本實現的程序的性能。
使用 Node 調用 WASM 程序(Worker Threads)
爲了讓我們的代碼保持簡單,我們可以將程序拆分爲三個部分:入口程序、Worker 程序、WASI 調用程序。還是先來實現入口程序 index.js
:
const { Worker } = require("worker_threads");
const { cpus } = require('os');
const { exit, pid } = require('process')
let dataStore = [];
const readyChecker = () => {
if (dataStore.length === numCPUs) {
console.log(`🎉 都搞定了,用時:${new Date - start}`);
exit(0);
}
}
const num = [40];
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * num.length);
const sharedArray = new Int32Array(sharedBuffer);
Atomics.store(sharedArray, 0, num);
const numCPUs = cpus().length;
console.log(`🚀 主進程上線 #ID ${pid}`);
const start = new Date();
for (let i = 0; i < numCPUs; i++) {
const worker = new Worker("./worker.js");
worker.on("message", msg => {
console.log("📦 收到結果", msg.ret)
dataStore.push(msg.ret);
readyChecker()
});
console.log(`👷 分發計算任務`);
worker.postMessage({ num: sharedArray });
}
接着實現 worker.js
部分:
const { parentPort } = require("worker_threads");
const wasi = require("./wasi");
parentPort.on("message", async (msg) => {
const instance = await wasi();
const ret = await instance.exports.fibonacci(msg.num)
parentPort.postMessage({ ret });
});
最後來實現新增的 wasi.js
部分:
const { readFileSync } = require('fs');
const { WASI } = require('wasi');
const { argv, env } = require('process');
module.exports = async () => {
const wasi = new WASI({ args: argv, env });
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
const wasm = await WebAssembly.compile(readFileSync("./module.wasm"));
const instance = await WebAssembly.instantiate(wasm, importObject);
wasi.start(instance);
return instance;
};
將文件都保存好之後,執行 node .
可以看到類似上面 Cluster 版本的運行輸出:
🚀 主進程上線 #ID 2927
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
👷 分發計算任務
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:2927) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
📦 收到結果 102334155
🎉 都搞定了,用時:825
和分別使用 Cluster 和 Threads 實現基準版程序一樣,執行時間也是差不多的。
對上述程序進行批量測試
爲了讓結果更客觀,我們來編寫一個小程序,讓上面的程序分別重複執行 100 遍,並剔除表現最好和最糟糕的情況,取平均值。
先來實現一個簡單的程序,代替我們的手動執行,針對不同的程序目錄執行不同的命令,收集 100 次程序運行數據:
const { execSync } = require('child_process');
const { writeFileSync } = require('fs');
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const cmd = './base';
const cwd = '../go-case';
const file = './go-base.json';
let data = [];
(async () => {
for (let i = 1; i <= 100; i++) {
console.log(`⌛️ 正在收集 ${i} 次結果`)
const stdout = execSync(cmd, { cwd }).toString();
const words = stdout.split('\n').filter(n => n && n.includes('搞定'))[0];
const time = Number(words.match(/\d+/)[0]);
data.push(time)
await sleep(100);
}
writeFileSync(file, JSON.stringify(data))
})();
程序執行之後的輸出會類似下面這樣:
⌛️ 正在收集 1 次結果
⌛️ 正在收集 2 次結果
⌛️ 正在收集 3 次結果
...
⌛️ 正在收集 100 次結果
在數據採集完畢之後,我們來編寫一個更簡單的程序,來查看程序運行的極端情況,以及排除掉極端情況後的平均值。(其實還應該跑個分佈,不過偷懶就不做啦,感興趣的同學可以試試看,會有驚喜)
const raw = require('./data.json').sort((a, b) => a - b);
const body = raw.slice(1,-1);
const sum = body.reduce((x,y)=>x+y, 0);
const average = Math.floor(sum / body.length);
const best = raw[0];
const worst = raw[raw.length-1];
console.log('最佳',best);
console.log('最差',worst);
console.log('平均', average);
測試結果
前文提到,本次測試可以看作上述程序在非常受限制的情況下的比較差的數據。
因爲這次我沒有使用雲服務器,而是使用筆記本上的本地容器,所以在持續運行過程中,由於多次密集計算,會導致設備的發熱。存在因爲溫度導致計算結果放大的情況,如果你採取雲端設備進行測試,數值結果應該會比較漂亮。
但是總體而言,趨勢還是很明顯的,或許足夠支撐我們在一些適合的場景下,採取 WASM + Node 或者 WASM + GO 的方式來進行混合開發了。
最後
如果說上一篇文章目的在於讓想要折騰 WASM 的同學快速上手,那麼這篇文章,希望能夠幫助到 “猶豫是否採用輕量異構方案” 的同學,並給出最簡單的實戰代碼。
因爲非常多的客觀原因,WASM 的發展和推廣或許會和 Docker 一般,非常的慢熱。在 2021 年的末尾,我們看到了 Docker 已經爆發出來的巨大能量。
如果你願意保持開放心態,如同七八年前,對待 Docker 一樣的來看待 WASM,相信這個輕量的、標準化的、異構的解決方案應該能爲你和你的項目帶來非常多的不同。
君子以文會友,以友輔仁。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1wIWfLJWXP4mxxJjkPMn8g