本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2021年11月26日 统计字数: 15696字 阅读时间: 32分钟阅读 本文链接: https://soulteary.com/2021/11/26/intensive-computing-with-wasm-part-1.html ----- # 借助 WASM 进行密集计算:入门篇 在[《使用 Docker 和 Golang 快速上手 WebAssembly》](https://soulteary.com/2021/11/21/use-docker-and-golang-to-quickly-get-started-with-webassembly.html)一文中,我介绍了如何制作符合 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 不足的状况。 关于构建程序使用的方式和容器环境,可以参考前一篇文章的“[环境准备](https://soulteary.com/2021/11/21/use-docker-and-golang-to-quickly-get-started-with-webassembly.html#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87-1)”,为了节约篇幅,就不多赘述了。 下面是各种依赖和组件的版本。 ```bash # 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 不做任何算法优化的话,纯计算函数的实现,代码大概会类似下面: ```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 程序](https://soulteary.com/2021/11/21/use-docker-and-golang-to-quickly-get-started-with-webassembly.html#%E7%BC%96%E5%86%99%E9%80%9A%E7%94%A8%E7%9A%84-wasi-%E7%A8%8B%E5%BA%8F),执行 `tinygo build --no-debug -o module.wasm -wasm-abi=generic -target=wasi wasm.go`。 我们将会顺利的得到一个编译好的、通用的 WASM 程序,用于稍后的程序测试中。 ## 编写 Go 语言基准版程序 虽然我们已经得到了 WASM 程序,但是为了直观的比较,我们还是需要一个完全使用 Go 实现基础版的程序: ```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`,将会看到类似下面的结果: ```go 102334155 🎉 都搞定了,用时:574 ``` 如果你想追求绝对的性能,可以进行 `go build base.go`,然后执行 `./base`,不过因为代码实在是太简单了,从输出结果来看,性能差异并不大。 基础计算功能搞定后,我们来简单调整代码,让代码具备并行计算的能力: ```go 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`,将会看到类似下面的结果: ```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 程序 ```go 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`,会得到类似下面的结果: ```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` 吧: ```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 .` ,程序输出内容会类似下面这样: ```js 🚀 主进程上线 #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](https://nodejs.org/api/worker_threads.html) 的能力。关于如何使用 Node.js 的 `Worker Treads` ,以及循序渐进的理解如何使用 `SharedArrayBuffer`,可以参考这篇文章[《How to work with worker threads in NodeJS》](https://livecodestream.dev/post/how-to-work-with-worker-threads-in-nodejs/),本文就不做基础展开了。考虑到我们模拟的是极限糟糕的情况,所以这里也不必使用 `node-worker-threads-pool` 之类的三方库,对 threads 做 pool 化了。(实测几乎没差别) 将上面 Cluster 版本的代码简单调整,我们可以得到类似下面的代码,不同的是,为了书写简单,我们这次需要拆分为两个文件: ```js 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` 的内容。 ```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 .` 同样可以看到类似下面的输出: ```js 🚀 主进程上线 #ID 2190 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 👷 分发计算任务 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 📦 收到结果 102334155 🎉 都搞定了,用时:1768 ``` 这里应该是因为**交换的数据量**比较少,所以执行时间其实和 `Cluster` 版本差不多。 ## 使用 Node 调用 WASM 程序(Cluster) 简单调整上文中的 Cluster 模式的代码,来实现一个能够使用 WASM 进行计算的程序: ```js 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 .` 后,会看到类似下面的输出结果: ```js 🚀 主进程上线 #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`: ```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` 部分: ```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` 部分: ```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 版本的运行输出: ```js 🚀 主进程上线 #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 次程序运行数据: ```js 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)) })(); ``` 程序执行之后的输出会类似下面这样: ```js ⌛️ 正在收集 1 次结果 ⌛️ 正在收集 2 次结果 ⌛️ 正在收集 3 次结果 ... ⌛️ 正在收集 100 次结果 ``` 在数据采集完毕之后,我们来编写一个更简单的程序,来查看程序运行的极端情况,以及排除掉极端情况后的平均值。(其实还应该跑个分布,不过偷懒就不做啦,感兴趣的同学可以试试看,会有惊喜) ```js 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); ``` ## 测试结果 前文提到,本次测试可以看作上述程序在非常受限制的情况下的比较差的数据。 因为这次我没有使用云服务器,而是使用笔记本上的本地容器,所以在持续运行过程中,由于多次密集计算,会导致设备的发热。存在因为温度导致计算结果放大的情况,**如果你采取云端设备进行测试,数值结果应该会比较漂亮**。 | 类型 | 最佳 | 最差 | 平均 | | --- | --- | --- | --- | | Go | 574 | 632 | 586 | | Go + WASM | 533 | 800 | 610 | | Node Cluster | 1994 | 3054 | 2531 | | Node Threads | 1997 | 3671 | 2981 | | Node Cluster + WASM | 892 | 1694 | 1305 | | Node Threads + WASM | 887 | 2160 | 1334 | 但是总体而言,趋势还是很明显的,或许足够支撑我们在一些适合的场景下,采取 WASM + Node 或者 WASM + GO 的方式来进行混合开发了。 ## 最后 如果说上一篇文章目的在于让想要折腾 WASM 的同学快速上手,那么这篇文章,希望能够帮助到“犹豫是否采用轻量异构方案”的同学,并给出最简单的实战代码。 因为非常多的客观原因,WASM 的发展和推广或许会和 Docker 一般,非常的慢热。在 2021 年的末尾,我们看到了 Docker 已经爆发出来的巨大能量。 如果你愿意保持开放心态,如同七八年前,对待 Docker 一样的来看待 WASM,相信这个轻量的、标准化的、异构的解决方案应该能为你和你的项目带来非常多的不同。 君子以文会友,以友辅仁。 --EOF