Akara's Wonderland

你好,Wasm!

2022-05-18 · 5 min read

说起来,有一个多月没写博客了(学习笔记不算),今天来整个活,整一个 wasm 项目并对比下相对于 JavaScript 的性能,不过由于我自己是个半吊子,我整不出一个完善的对比环节,所以后面比较性能的内容可能有诸多纰漏敬请谅解。

WebAssembly(缩写为Wasm)是基于堆栈的虚拟机的二进制指令格式。Wasm 被设计为编程语言的可移植编译目标,支持在 Web 上部署客户端和服务器应用程序。

安装环境

wasm 需要从一种语言编译成二进制码,我们这里选 C 语言,由于今天只是浅尝辄止,不想污染我的 Windows 开发环境,所以今天的案例跑在 wsl。

需要的软件列表如下:

  1. Git:版本管理工具,wasm 的编译器需要我们编译,仓库需要拿 Git 克隆下来
  2. CMake:开源的跨平台自动化建构系统
  3. 编译器:编译 C 程序的
  4. Python 2.x:sdk 实际上是 python 写的,所以...

我用的 Ubuntu 系统,其他系统的自便,另外别忘了换清华源。

apt install build-essential cmake python git

好,依赖安装好了,接下来编译

git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

中间看到了某 404 公司的资源被下载,所以我们需要做什么呢?我也不知道。

如果现在一切顺利,那么 emcc 编译器已经成功安装了,如果你在 bash 中键入 emcc,应该不会看到 command not found 之类的东西。

第一个 wasm 应用

首先先创建文件和目录:

mkdir first-wasm && cd first-wasm
touch main.c

在 main.c 里随便写个 Hello World:

#include <stdio.h>

int main() {
    printf("Hello World\n");
}

然后编译吧:

emcc main.c -o hello.html

-o 参数决定输出到哪个 HTML 里,这个编译工具直接帮你把所有 js 都写好了,直接给你送到 html 里。

如果你使用文件资源管理器双击大法打开文件,会发现控制台里因为 CORS 问题导致 wasm 文件没法加载,所以我们需要整个开发服务器,用 http 协议访问

npm install http-server -g
http-server hello.html

然后打开 http://localhost:8080/hello.html,就能看见你的第一个 wasm 应用跑起来了。

性能对比

之前一直好奇到底这俩性能的差距能有多大,今天来简单比一下。再强调一次,我水平拉跨的要死,这种性能对比看个乐呵就行了。

比较的内容是,分别计算斐波那契数列的第30项、第40项、第50项各十次,并给出计算这三个数字的平均时间。设备是 ROG 幻 16,CPU 是 Intel 11th i7,运行在 Chrome 101.0.4951.67(正式版),不能保证实验数据非常准确,因为这只是在找乐子。

JavaScript 上场

这里我实际上是拿 TypeScript 写的:

const execTime = (func: () => any) => {
    let start = new Date().getTime();
    func();
    let end = new Date().getTime();
    return end - start;
}

const benchmark = (func: () => any) => {
    const time = 10;
    let result: Array<number> = [];
    for (let i = 0; i <= time; i++) result.push(execTime(func));
    let sum = result.reduce((prev, next) => prev + next);
    return sum / time;
}

const fibonacci = (n: number): number => {
    if (n == 1 || n == 2) return 1;
    return fibonacci(n - 2) + fibonacci(n - 1);
}

const fibonacciBenchmark = (start: number, end: number, range: number) => {
    let map = new Map();
    for (let index = start; index <= end; index += range) {
        map.set(index, benchmark(() => fibonacci(index)))
    }
    return map;
}

console.log(fibonacciBenchmark(30, 50, 10));

最后的结果是:

  1. 计算 30,平均时间是 5.9ms
  2. 计算 40,平均时间是 677.5ms
  3. 计算 50,平均时间是 101165.9ms

不出意料,还算凑合。

C 语言上场

用到的代码是 Expliyh 帮忙写的:

#include <stdio.h>
#include <time.h>

long long fibo(long long n);

int main()
{
    clock_t start, end;
    start = clock();
    for (int i = 0; i < 10; i++) {
        // fibo(30);
        // fibo(40);
        // fibo(50);
    }
    end = clock();
    printf("%lld\n", m);
    printf("%ld", (end - start) / 10);
}

long long fibo(long long n)
{
    if (n <= 1)
    {
        return n;
    }
    return fibo(n - 1) + fibo(n - 2);
}

结果是:

  1. 计算 30,平均时间是 5ms
  2. 计算 40,平均时间是 566ms
  3. 计算 50,平均时间是 71988ms

上个表格对比一下吧:

JavaScript C
30 5.9ms 5ms
40 677.5ms 566ms
50 101165.9ms 71988ms

好像也没快太多...就那样?不过这目前还没上多线程,可惜我暂时无法实现,只能待之后再议。待我 Golang 大成,必然要好好折腾 wasm 一次。