随着 Web 应用的不断发展和日益复杂化,性能优化成为了开发过程中一个不可忽视的重点。WebAssembly (Wasm) 提供了一种新的方法,通过允许在浏览器中运行编译型语言(如 C/C++)来加速 JavaScript 应用,尤其是在处理计算密集型任务时。本文将介绍如何使用 C/C++ 开发斐波那契数列计算功能,编译为 WebAssembly,并在前端 JavaScript 中进行调用和性能对比。

什么是 WebAssembly?

WebAssembly 是一种新的代码格式,旨在为现代网络应用提供新的编码方式,使得代码可以以接近原生的速度执行。它是一种低级语言,既可以作为目标语言直接手动编写,也可以作为编译目标,使得你可以使用如 C、C++ 等高级语言编写应用逻辑。

环境准备

在开始之前,确保你的开发环境中已安装以下工具:

  • Emscripten: 一个编译器工具链,可以将 C/C++ 代码编译成 WebAssembly。
  • Node.js: 用于运行 JavaScript 代码。

配置emcc全局环境

如果不配置全局环境变量,每次新开命令行窗口使用 emsdk 工具时,都需要执行一次 source ./emsdk_env.sh 。配置环境变量后在任何窗口都可以使用,比较方便。

打开环境变量配置文件:

vim ~/.zshrc

在后面加入如下信息,即 emsdk 和 emscripten 的安装目录加到 path 中。

export EMSDK_PATH="/Users/path/to/emsdk"
export EMSCRIPTEN_PATH="/Users/path/to/emsdk/upstream/emscripten"
export PATH=$PATH:${EMSDK_PATH}
export PATH=$PATH:${EMSCRIPTEN_PATH}

步骤 1: 编写 C/C++ 代码

首先,我们创建一个 C++ 程序来计算斐波那契数列:

// fibonacci.cpp
extern "C"
{
    int fibonacci(int n)
    {
        if (n <= 1)
            return n;
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

步骤 2: 编译为 WebAssembly

使用 Emscripten 编译器将上述 C++ 代码编译为 WebAssembly:

emcc fibonacci.cpp -s WASM=1 -o fibonacci.js
# emcc -o fibonacci.js fibonacci.cpp -s EXPORTED_FUNCTIONS='["_fibonacci"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'

这条命令将生成两个文件:fibonacci.js(加载和初始化 WebAssembly 模块的 "胶水" 代码)和 fibonacci.wasm(包含编译后的 WebAssembly 代码)。

步骤 3: JavaScript 实现对比

为了对比性能,我们同样实现 JavaScript 版本的斐波那契数列计算:

function fibonacciJS(n) {
    if (n <= 1) return n;
    return fibonacciJS(n - 1) + fibonacciJS(n - 2);
}

步骤 4: 在网页中使用 WebAssembly

创建一个简单的 HTML 页面来加载我们的 JavaScript 代码,并测试两种实现的性能:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Fibonacci Calculator</title>
</head>
<body>
    <script src="fibonacci.js"></script>
    <script>
        // 当 WebAssembly 模块加载完成
        Module.onRuntimeInitialized = async function() {
            const n = 40; // 斐波那契数列计算项数

            console.time('Fibonacci JavaScript');
            console.log('Fibonacci JS Result:', fibonacciJS(n));
            console.timeEnd('Fibonacci JavaScript');

            console.time('Fibonacci WebAssembly');
            console.log('Fibonacci WASM Result:', Module._fibonacci(n));
            console.timeEnd('Fibonacci WebAssembly');
        };
    </script>
</body>
</html>

性能对比与总结

打开这个 HTML 文件后,你可以在浏览器的控制台看到两种方法执行的时间。通过比较,你将发现在执行复杂的递归计算任务时,WebAssembly 版本通常比 JavaScript 版本有更优的性能表现。

通过这个例子,我们可以看到 WebAssembly 在前端开发中的潜力,尤其是在处理那些计算密集型的任务时。这为前端应用带来了新的可能性,开发者可以利用现有的 C/C++ 代码库,提升应用性能,优化用户体验。

Demo

extern "C"
{
    int fibonacci(int n)
    {
        // 循环求自然数从0到n的和
        int sum = 0;
        for (int i = 1; i <= n; i++)
        {
            sum += i;
        }

        return sum;
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Fibonacci Calculator</title>
</head>
<body>
  <script>
    // 异步加载 WebAssembly 模块并执行
    async function loadAndExecuteWasm() {
      const response = await fetch('fibonacci.wasm');
      const buffer = await response.arrayBuffer();
      const module = await WebAssembly.instantiate(buffer);
      const result = module.instance.exports.fibonacci(9999999999); // Wasm 模块有一个名为 fibonacci 的函数 (与cpp中的命名相同)
    }

    // 循环求自然数从0到n的和
    function sumOfNaturalNumbersJS(n) {
      let sum = 0;
      for (let i = 1; i <= n; i++) {
        sum += i;
      }
      return sum;
    }

    // 页面加载后执行 JavaScript 和 WebAssembly 比较
    window.onload = async function() {
      const n = 9999999999;

      console.time('JavaScript');
      sumOfNaturalNumbersJS(n)
      console.timeEnd('JavaScript');

      console.time('WebAssembly');
      await loadAndExecuteWasm();
      console.timeEnd('WebAssembly');
    };
  </script>
</body>
</html>
# e.g. output:
JavaScript: 9946.489013671875 ms
WebAssembly: 4198.52587890625 ms

相关链接


A Student on the way to full stack of Web3.