在本章中,我们将学习 Emscripten,这是一个将 C/C++ 代码转换为 WebAssembly 模块的工具链。
Emscripten 由两部分组成:
- Emscripten 编译器前端
- Emscripten SDK(emsdk)
Clang 编译器前端将 C/C++ 代码编译成LLVM 中间表示(LLVM IR),然后使用 LLVM 后端将 LLVM IR 转换成原生代码。Clang 编译器速度快,使用的内存少,且与GNU 编译器集合(GCC)兼容。Emscripten 类似于 Clang;前者生成 wasm 二进制文件,而后者生成 原生 二进制文件。Emscripten 编译器前端(emcc)是将 C/C++ 转换成 LLVM IR(二进制和人类可读形式)以及 WebAssembly 二进制或 asm.js 的编译器前端,如 JavaScript。
图2.1 – Emscripten 编译器前端
emsdk 帮助管理和维护 Emscripten 工具链组件,并设置运行时/终端环境以运行 emcc。
在本章中,我们将学习如何安装 Emscripten。然后,我们将使用 Emscripten 生成 asm.js,一个可以在 Node.js 和浏览器上运行的 WebAssembly 模块。之后,我们将探索 emsdk 工具。最后,我们将探索 Emscripten 提供的各种优化。我们将在本章中涵盖以下主题:
-
使用 emsdk 安装 Emscripten
-
使用 Emscripten 生成 asm.js
-
在 Node.js 中运行 Emscripten 的 Hello World
-
在浏览器中运行 Emscripten 的 Hello World
-
探索 emsdk 中的其他选项
-
理解各种级别的优化
您可能不知道的是
asm.js 是 JavaScript 的一个子集,优化后可以在浏览器中以接近原生的性能运行。asm.js 规范并没有被所有浏览器厂商接受。asm.js 已经发展成了 WebAssembly。
技术要求
我们将在本章中展示如何设置 Emscripten,您将需要在您的系统上安装以下内容:
-
Python >= 3.7
-
Node.js > 12.18
注意
emsdk 自带一个兼容的 Node.js 版本。
技术要求
本章将展示如何设置Emscripten,为此您需要在您的系统上安装以下内容:
-
Python >= 3.7
-
Node.js > 12.18
注意
emsdk预装了兼容的Node.js版本。
使用emsdk安装Emscripten
emsdk提供了一种简单的方法来安装、管理以及切换Emscripten工具链的版本。emsdk负责设置编译C/C++到LLVM IR,然后到JavaScript(以asm.js或WebAssembly二进制格式)所需的环境、工具和SDK。
现在让我们安装Emscripten并开始入手:
-
克隆emsdk仓库并进入
emsdk
文件夹:$ git clone https://github.com/emscripten-core/emsdk $ cd emsdk
-
要在机器上安装emsdk,请运行以下命令:
对于*nix用户,请使用以下命令:
$ ./emsdk install latest
对于Windows用户,请使用以下命令:
$ emsdk install latest
注意
前述命令可能需要一些时间来运行;它将构建并设置整个工具链。
接下来,我们将激活最新版本的emsdk。激活将更新本地shell,添加所需的环境引用,并使当前shell中的用户使用最新的SDK。它会将Emscripten工具链的路径和其他必要信息写入用户主目录下名为.emscripten
的文件:
- 要激活已安装的emsdk,请运行以下命令:
对于*nix用户,请使用以下命令:
$ ./emsdk activate latest
对于Windows用户,请使用以下命令:
$ emsdk activate latest
- 现在,是时候确保配置和路径被激活,通过运行以下命令:
对于*nix用户,请使用以下命令:
$ source ./emsdk_env.sh
对于Windows用户,请使用以下命令:
$ emsdk_env.bat
恭喜,Emscripten工具链已安装!使用emsdk更新工具链就像安装一样简单。
要进行更新,请运行以下命令:
对于*nix用户,请使用以下命令:
$ ./emsdk update
对于Windows用户,请使用以下命令:
$ emsdk update
emsdk在Emscripten配置文件中设置了以下路径。Emscripten配置文件(.emscripten
)位于家目录中。它包含以下内容:
LLVM_ROOT
– 指定LLVM Clang编译器的路径NODE_JS
– 指定Node.js的路径BINARYEN_ROOT
– 指定Emscripten编译器的优化器EMSCRIPTEN_ROOT
– 指定Emscripten编译器的路径
我们可以通过使用以下命令来检查emcc的安装是否成功:
$ emcc --version
现在我们已经完成了Emscripten编译器的安装,让我们继续使用它。
使用Emscripten生成asm.js
我们将使用Emscripten将C/C++程序移植到asm.js或WebAssembly二进制格式,然后在JavaScript引擎中运行它们。
注意
像Lua和Python这样的编程语言具有C/C++运行时。借助Emscripten,我们可以将运行时作为WebAssembly模块移植,并在JavaScript引擎中执行它们。这使得在JavaScript引擎上运行Lua/Python代码变得容易。因此,Emscripten和WebAssembly允许在JavaScript引擎中运行原生代码。
首先,让我们创建一个sum.cpp
文件:
// sum.cpp
extern "C" {
unsigned sum(unsigned a, unsigned b) {
return a + b;
}
}
将extern "C"
视为一种类似导出机制。其中的所有函数都可以作为导出函数而不改变它们的名称。然后,我们定义了正常的sum
函数,它接受两个数字并返回一个数字。
为了从sum.cpp
生成类似asm.js的JavaScript代码,请使用以下命令:
$ emcc -O1 ./sum.cpp -o sum.html -sWASM=0 -sEXPORTED_FUNCTIONS='["_sum"]'
注意
如果这是您第一次运行emcc,它可能需要几秒钟才能完成。后续运行将会更快。
我们向emcc编译器传递-O1
选项,指示编译器产生较少优化的代码(我们将在本章后面看到更多优化选项)。接下来,我们传递要转换的文件,即sum.cpp
。然后,使用-o
标志,我们提供输出的期望名称,即sum.html
。
最后,我们使用-s
标志向emcc编译器发送更多信息。-s
标志以键值对的形式接受参数。emcc编译器默认生成WebAssembly模块。WASM=0
指示编译器生成类似asm.js的JavaScript,而不是WebAssembly。
然后,我们使用EXPORTED_FUNCTIONS
选项指定导出的函数。EXPORTED_FUNCTIONS
选项接受一个参数数组。为了导出sum
函数,我们指定_sum
。
这将生成以下代码:
/// $1 映射到 _sum
function $1($0_1, $1_1) {
$0_1 = $0_1|0;
$1_1 = $1_1|0;
return $0_1 + $1_1 | 0;
}
注意
|0
指定类型为数字。
现在在浏览器中打开sum.html
并打开开发者控制台。为了调用导出的函数,我们将在控制台中运行以下表达式:
ccall("sum", "number", "number, number", [10, 20])
// 输出 30
ccall
是从JavaScript通过C/C++代码调用导出函数的方式。该函数接受函数名称、返回值类型、参数类型,然后作为数组输入参数。这将调用sum
函数以产生结果。我们将在后面的章节中了解更多关于ccall
和cwrap
的信息。但就目前而言,可以将ccall
视为调用C函数的方式。
更多关于Emscripten源代码的信息,请访问 https://github.com/emscripten-core/emscripten。
到目前为止,我们已经看到了如何使用emscripten生成asm.js文件。现在,让我们使用emscripten创建一个在Node.js上运行的WebAssembly模块。
在Node.js中使用Emscripten运行Hello World
在本节中,我们将看到如何通过Emscripten将C/C++代码转换为WebAssembly二进制文件,并与Node.js一起运行它。
注意
如果终端报错显示emcc command not found,您的终端环境可能已被重置。要设置环境,请在emsdk
文件夹内运行以下命令:
source ./emsdk_env.sh
让我们遵循Brian Kernighan的传统,写一个“Hello, world”程序,但稍作变化。我们来做一个“Hello, Web”:
-
首先,我们创建一个
hello_web.c
文件:$ touch hello_web.c
-
打开您喜欢的编辑器并添加以下代码:
#include <stdio.h> int main() { printf("Hello, Web!\n"); return 0; }
这是一个简单的C程序,包含一个main
函数。main
函数是运行时的入口点。当使用Clang(clang sum.c && ./a.out
)编译并执行这段代码时,会打印“Hello, Web!”。现在,让我们使用emcc而不是Clang(或其他编译器)来编译这段代码。
-
我们输入以下命令用emcc来编译代码:
$ emcc hello_web.c
完成后,将生成以下文件:
a.out.js
a.out.wasm
生成的JavaScript文件非常庞大。它有2000多行,大小为109 KB。我们将在本章后面学习如何优化文件大小。
-
让我们使用Node运行生成的JavaScript文件,它将打印出“Hello, Web!”:
$ node a.out.js Hello, Web!
恭喜您!您刚刚运行了您的第一个WebAssembly二进制文件!
注意
在浏览器的世界里,二进制大小很重要。如果您的代码很庞大,即使您的算法以纳秒速度运行,也没有意义。浏览器在开始解析和编译之前,需要等待接收到所有必要的信息。因此,检查文件大小是必须的。Closure Compiler有助于进一步减小字节码大小。Closure Compiler不仅减小了代码大小,还试图使代码更高效。
生成的JavaScript文件包含了自己的运行时和配置,这些是JavaScript引擎执行JavaScript引擎内的WebAssembly模块所需的。生成的JavaScript文件为浏览器和Node.js创建了一个JavaScript模块并初始化代码:
- 在Node.js中,生成的JavaScript文件通过从本地文件系统读取文件来创建模块。它获取传递给node命令的参数,并在创建的模块中设置它们。
- 在浏览器中,生成的JavaScript文件通过构建请求并从URL获取字节来创建模块。浏览器从托管服务器或位置获取WebAssembly二进制文件,然后实例化模块。
生成的JavaScript文件还创建了堆栈、内存、导入和导出部分。我们将在本书后面深入了解这些部分。
这个生成的JavaScript文件被称为绑定文件。绑定文件的主要功能是创建或设置一个环境,使得JavaScript引擎内可以执行WebAssembly模块。绑定文件充当JavaScript和WebAssembly之间的翻译者。所有的值都通过这个绑定文件传入和传出。
当通过node执行JavaScript文件时,它会做以下事情。
JavaScript引擎首先加载模块,然后设置WebAssembly执行所需的常量和各种函数。然后,模块检查代码执行的位置,无论模块是在浏览器内还是在Node
环境中。基于此,它获取文件。由于我们在这里通过node运行WebAssembly模块,因此它从本地文件系统获取文件。然后,模块检查调用是否提供了任何参数。如果没有,JavaScript引擎将检查是否有任何未处理/未捕获的异常。JavaScript引擎然后将print out
/print err
函数映射到控制台。JavaScript引擎检查模块是否加载了所有执行所需的访问权限和全局变量和导入。
模块接着初始化堆栈和其他所需常量,以及用于分别解码和编码缓冲区的解码器和编码器。编码器负责将JavaScript值转换为WebAssembly能够理解的值。解码器负责将WebAssembly值转换为JavaScript能够理解的值。
Node.js运行时然后检查文件的可用性,然后初始化文件。模块检查所有与WebAssembly相关的函数的可用性。一旦一切都初始化完毕,且模块包含所有所需函数,我们将调用run
函数。
run
函数实例化WebAssembly二进制文件。在这种情况下,由于我们在C中定义了main
函数,因此绑定文件在实例化时直接调用main
函数。
绑定文件包含ccall
函数。ccall
函数是对C中定义的底层函数的接口:
function ccall(ident, returnType, argTypes, args, opts) {
// 代码已省略
}
ccall
函数接受以下参数:
ident
– 要调用的函数;它是在C中定义的函数标识符。returnType
– 函数的返回类型。argTypes
– 参数类型。args
– 随函数调用传递的参数。opts
– 任何其他所需的选项。
JavaScript模块除了ccall
外还导出了cwrap
函数。cwrap
是ccall
函数的封装函数。虽然ccall
是函数调用,但cwrap
提供了一个调用ccall
的函数:
function cwrap(ident, returnType, argTypes, opts) {
return function() {
return ccall(ident, returnType, argTypes,
arguments, opts);
}
}
生成的WebAssembly文件包含二进制操作码,指示运行时打印“Hello, Web!”。WebAssembly文件以00 61 73 6d 01 00 00 00
开头。
有关WebAssembly规范的更多信息,请访问 https://webassembly.github.io/spec/。
到目前为止,我们已经看到了如何生成一个在Node.js上运行的WebAssembly模块。让我们使用emscripten在浏览器中创建一个WebAssembly模块来运行。
在浏览器中使用Emscripten运行Hello World
在本节中,我们将看到如何通过Emscripten将C/C++代码转换为WebAssembly二进制文件,并在浏览器中运行它。
注意
如果终端显示找不到emcc
命令,那么您可能忽略了设置环境变量。要设置环境变量,请在emsdk
文件夹内运行以下命令:source ./emsdk_env.sh
让我们使用使用Emscripten生成asm.js部分中使用的同样的代码示例。现在,我们不仅运行emcc,还传递-o
选项并指示emcc生成.html
文件:
$ emcc hello_web.c -o helloweb.html
完成后,将生成以下文件:
helloweb.js
helloweb.wasm
helloweb.html
类似于Node示例,生成的JavaScript文件非常庞大。我们将在本章后面学习如何优化文件大小。
注意
-o
选项确保所有生成的文件都具有名称helloweb
。
为了在浏览器中运行生成的HTML文件,我们需要一个Web服务器。Web服务器通过HTTP协议提供HTML文件。解释Web服务器及其工作原理超出了本书的范围;有关更多详细信息,请参阅Web服务器 - 维基百科。
Python提供了一个简单的方法来运行Web服务器。为了使用Python运行Web服务器,请运行以下命令:
$ python -m http.server <端口号>
打开http://localhost:<端口号>/helloweb.html
在浏览器中查看WebAssembly的运行情况。
图 2.2 - 运行 WebAssembly 的浏览器
当通过浏览器执行 JavaScript 文件时,它会输出 Hello, Web!。唯一的区别在于,它不是从文件系统加载 WASM 文件,而是通过 XHR 加载。一旦一切初始化完成,模块包含了所有所需的函数,我们将调用 run
函数。run
函数会实例化 WebAssembly 二进制文件。在这个案例中,由于我们用 C 语言定义了 main
函数,所以绑定文件在实例化时直接调用 main
函数。
Emscripten 还提供了 emrun
来运行 HTML 文件。更多信息请查看。
有关部署 Emscripten 编译页面的更多信息,请访问 https://emscripten.org/docs/compiling/Deploying-Pages.html。
我们已经使用 Emscripten 生成了 WebAssembly 模块。让我们继续探索 emsdk 还能做些什么。
探索 emsdk 中的其他选项
emsdk 是一个一站式的工具,用于安装、维护和管理使用 Emscripten 所需的所有工具和工具链。emsdk 使得引导环境、升级到最新版本、切换到不同版本、更改或配置各种工具等变得更加简单。
emsdk
命令位于 emsdk
文件夹中。进入 emsdk
文件夹并运行 emsdk
命令。
注意
在本章节中的所有命令,对于 *nix 系统,使用 ./emsdk
,对于 Windows,使用 emsdk
。
要找出 emsdk
命令中可用的各种选项,请运行以下命令:
$ ./emsdk --help
emsdk: 可用命令:
emsdk list [--old] [--uses] - 列出工具
emsdk update - 将 emsdk 更新到最新版本。
emsdk update-tags - 从 GitHub 仓库获取最新标签。
emsdk install - 安装工具和 SDK。
emsdk uninstall - 卸载先前安装的工具和 SDK。
emsdk activate - 激活当前安装的版本。
一个 emsdk
命令的格式如下:
emsdk <option> <工具/SDK> --<flags>
emsdk
命令包括以下内容:
<option>
这可以是以下之一:list、update、update-tags、install、uninstall 或 activate。
<工具/SDK>
这指的是库,包括 Emscripten 和 LLVM。SDK
指的是 emsdk 本身。
--<flags>
这指的是各种配置选项。
让我们探索 emsdk 命令支持的每一个选项和标志。
列出工具和 SDK
在这里,我们展示如何列出 emsdk 中可用的工具和 SDK。运行以下命令:
$ ./emsdk list
The *recommended* precompiled SDK download is 2.0.6
(4ba921c8c8fe2e8cae071ca9889d5c27f5debd87).
To install/activate it, use one of:
latest [default (llvm) backend]
latest-fastcomp [legacy (fastcomp) backend]
Those are equivalent to installing/activating the following:
2.0.6 INSTALLED
2.0.6-fastcomp
All recent (non-legacy) installable versions are:
2.0.6 INSTALLED
...
The additional following precompiled SDKs are also available
for download:
sdk-fastcomp-1.38.31-64bit
The following SDKs can be compiled from source:
sdk-upstream-master-64bit
...
The following precompiled tool packages are available for
download:
...
* node-12.18.1-64bit INSTALLED
* python-3.7.4-2-64bit INSTALLED
emscripten-1.38.30
...
The following tools can be compiled from source:
llvm
clang
emscripten
binaryen
Items marked with * are activated for the current user.
To access the historical archived versions, type 'emsdk list
--old'
Run "git pull" followed by "./emsdk update-tags" to pull
in the latest list.
open_url("file-tL0suvKqhxGxCrTb2bA9LZZ7")
emsdk list
命令列出了所有可用的工具包和 SDK。这些工具和 SDK 的列表包括 LLVM、Clang、Emscripten 和 Binaryen 的最近几个版本。它们甚至包括了 Node 版本 8 和 12,以及 Python 3.7。emsdk 负责维护和管理 emsdk
。这意味着我们需要了解我们正在使用的当前版本的信息以及如何更新它。emsdk list
命令还提供了关于 SDK 组件的更多详细信息,以及那些从源代码编译的列表。
管理工具和 SDK
emsdk 提供了安装、更新和卸载工具和 SDK 的选项。
为了安装工具、SDK 或 emsdk 本身,请使用以下命令:
$ ./emsdk install <要安装的工具/SDK>
要安装 SDK 的最新版本,可以运行以下命令:
./emsdk install latest
注意
latest
指的是 emsdk 的最新版本。
要使用 emsdk install
命令安装多个工具,请使用以下命令:
./emsdk install <tool1> <tool2> <tool3>
您也可以为 install
命令指定多个选项。您可以这样传递选项给 install
命令:
./emsdk install [options] <tools / SDK>
可用的各种 options
有:
- 构建所用的核心数
- 构建类型
- 工具和 SDK 的激活
- 卸载
构建所用的核心数
初次设置将花费很长时间来构建和安装所需的工具和 SDK。根据您的需求,您可以控制构建和安装所需工具和 SDK 的核心数:
./emsdk install -j<用于构建的核心数> <tools
/ SDK>
构建类型
您可以指导 emsdk
使用什么类型的构建来使 LLVM 表现出来:
./emsdk install --build=<type> <tools / SDK>
type
接受以下选项:
-
调试
- 用于调试。
- 生成符号文件。
- 最终构建不会产生优化、快速代码。
-
发布
- 这种类型将生成优化、快速代码。
-
MinSizeRel
- 与发布相同。
- 这种类型将最小化大小并最大化速度。
- 使用优化选项,如
-O1
(最小化大小)和-O2
(最大化速度)。
-
RelWithDebInfo
- 与发布相同。
- 这种类型还会生成符号文件,有助于调试。
激活工具和 SDK
工具和 SDK 安装后,我们可以激活不同的版本来使用它们。activate
命令生成必要的配置文件,将路径映射到构建的可执行文件。
要激活工具和 SDK,请运行以下命令:
./emsdk activate <要激活的工具/SDK>
activate
命令接受一些选项,如下:
--embedded
– 这个选项确保所有构建的文件、配置、缓存和临时文件都位于 emsdk 命令所在的目录内。
如果没有指定,这个命令会将配置文件移动到用户的主目录。
--build=<type>
– 类似于 LLVM 支持的构建类型。例如,调试、发布、MinSizeRel、RelWithDebInfo。
卸载工具和 SDK
要卸载工具和 SDK,我们可以运行以下命令:
./emsdk uninstall <要卸载的工具/SDK>
有关工工具的更多信息请访问 Emscripten 文档。
我们已经探讨了如何使用 emsdk 来管理工具和 SDK;接下来让我们探索由 Emscripten 提供的各种优化功能。
理解不同级别的优化
C/C++ 程序通过 Clang 或 GCC 编译器编译并转换为本地代码。Clang 或 GCC 编译器根据目标将 C/C++ 程序转换。这里的目标指的是代码执行的最终机器。emcc 内置了 Clang 编译器。emcc 编译器负责将 C 或 C++ 源代码转换为 LLVM 字节码。
在本节中,我们将看到如何提高生成的 WebAssembly 二进制代码的优化和代码大小。
为了提高效率和生成的代码大小,Emscripten 编译器提供了以下选项:
- 优化
- 闭包编译器
首先讨论优化。
优化
编译器的目标是减少编译成本,即编译时间。使用 -O
优化标志,编译器试图在牺牲编译时间的情况下改进代码大小和/或性能。在编译器优化方面,代码大小和性能是相互排斥的。编译时间越快,优化程度越低。要指定优化,我们使用 -O<0/1/2/3/s/z>
标志。每个选项包括各种断言、代码大小优化和代码性能优化等。
以下是各种优化选项:
-O0
– 这是默认选项,是实验的完美起点。这个选项意味着“无优化”。这个优化级别编译最快,生成最可调试的代码。这是基本的优化级别。这个选项试图内联函数。-O1
– 这个选项添加简单的优化,并试图生成最小的代码大小。这个选项删除了生成代码中的运行时断言,并且比-O0
选项构建得更慢。这个选项还试图简化循环。-O2
– 这个选项添加比-O1
更多的优化。它比-O1
慢,但生成的代码比-O1
选项更优化。这个选项基于 JavaScript 优化优化代码,并删除不属于 JavaScript 模块的代码。这个选项删除内联函数,vectorize-loop
选项被设置。这个选项添加适度的优化。这个选项还增加了死代码消除。
向量化将指示处理器分块执行操作,而不是一次一个。
-O3
– 这个选项添加更多选项,编译时间更长,并生成比-O2
选项更优化的代码。
这个选项生成最佳的生产就绪代码。这个选项就像 -O2
,除了它启用了更长时间执行的优化,或可能生成更大的代码(试图使程序运行得更快)。
-Os
– 这个选项类似于-O2
。它增加了额外的优化并减少了代码大小。减少代码大小反过来会降低性能。这个选项生成的代码比-O2
小。-Oz
– 这个选项类似于-Os
,但进一步减少了代码大小。这个选项需要更长的编译时间来生成二进制代码。
现在我们将探索 Emscripten 提供的各种优化选项:
-
open_url("file-4WE3SZEZqZrx3gAR8Aj5Svir")
好的,让我们继续对您提供的内容进行润色翻译。
-
首先,我们创建一个名为
optimization_check
的 C 文件:$ touch optimization_check.c
-
然后,打开您最喜欢的编辑器并添加以下代码。以下是一个简单的 C 文件,包含一个
main
函数和其他几个函数:#include <stdio.h> int addSame(int a) { return a + a; } int add(int a, int b) { return a + b; } int main() { printf("Hello, Web!\n"); int a; int sum = 0; /* for 循环执行 */ for( a = 0; a < 20; a = a + 1 ){ sum = sum + a; } addSame(sum); add(1, 2); return 0; }
-
接着我们使用 emcc 将其编译为 WebAssembly 代码:
$ time emcc optimization_check.c emcc optimization_check.c 0.32s user 0.14s system 90% cpu 0.514 total
-
然后,检查生成的文件大小:
$ l 324B optimization_check.c 13K a.out.wasm 109K a.out.js
我们可以看到生成的 WebAssembly 文件约为 13 KB,编译总共用时 0.514 秒。这是快速的编译,但代码大小却很大。
在编译器的世界里,编译越快,代码大小越大,执行速度越慢。
-
现在,让我们使用
-O1
选项进一步优化它:$ time emcc -O1 optimization_check.c emcc -O1 optimization_check.c 0.31s user 0.13s system 86% cpu 0.519 total
检查生成的文件大小:
$ l 324B optimization_check.c 3.4K a.out.wasm 59K a.out.js
生成的 WebAssembly 文件约为 3.4 KB(比
-O0
版本小 3.8 倍),用时几乎相同,约为 0.519 秒。-
现在,让我们使用
-O2
选项进一步优化它:$ time emcc -O2 optimization_check.c emcc -O2 optimization_check.c 0.53s user 0.16s system 111% cpu 0.620 total
检查生成的文件大小:
$ l 324B optimization_check.c 2K a.out.wasm 20K a.out.js
生成的 WebAssembly 文件约为 2 KB(比
-O0
小约 6.5 倍),用时约为 0.62 秒。-
现在,让我们使用
-O3
选项进一步优化它:$ time emcc -O3 --profiling optimization_check.c emcc -O3 --profiling optimization_check.c 1.03s user 0.21s system 110% cpu 1.117 total
有关
--profiling
标志的更多信息,请访问 https://emscripten.org/docs/tools_reference/emcc.html#emcc-profiling。检查生成的文件大小:
$ l 324B optimization_check.c 2.0K a.out.wasm 17K a.out.js
生成的 WebAssembly 文件大小与
-02
相同,但生成的 JavaScript 文件少了 3 KB,编译用时约为 1.117 秒。-
现在,让我们使用
-Os
选项进一步优化它:$ time emcc -Os optimization_check.c emcc -Os optimization_check.c 1.03s user 0.22s system 46% cpu 2.655 total
检查生成的文件大小:
$ l 324B optimization_check.c 1.7K a.out.wasm 14K a.out.js
生成的 WebAssembly 文件约为 1.7 KB(比
-O0
小约 7.5 倍),用时几乎为 2.655 秒。-
现在,让我们使用
-Oz
选项进一步优化它:$ time emcc -Oz optimization_check.c emcc -Oz optimization_check.c 1.03s user 0.21s system 110% cpu 1.123 total
检查生成的文件大小:
$ l 324B optimization_check.c 1.7K a.out.wasm 14K a.out.js
生成的 WebAssembly 文件约为 1.7 KB(比
-O0
小约 7.5 倍),用时约为 1.123 秒。接下来,我们将看到 Emscripten 编译器提供的另一种提高效率和减小生成代码大小的方法:闭包编译器(Closure Compiler)。
闭包编译器(Closure Compiler)
闭包编译器是一种将 JavaScript 编译为更好 JavaScript 的工具。它解析、分析、移除死代码、重写并缩小 JavaScript。使用闭包编译器对生成的绑定 JavaScript 文件和 WebAssembly 模块进行进一步优化。使用闭包编译器,我们可以对 Emscripten 代码进行更好的优化。为了进一步优化 WebAssembly 模块和 JavaScript,我们可以使用
--closure <optimization type>
。optimization
类型有以下选项:--closure 0
– 这个选项不添加闭包编译器优化。--closure 1
– 这个选项减少了生成的 JavaScript 代码大小。这个选项不优化 asm.js 和 WebAssembly 二进制文件。这增加了额外的编译步骤,从而增加了编译时间。--closure 2
– 这个选项优化 JavaScript、asm.js,但不是 WebAssembly 二进制文件,并大幅减少 asm.js 文件的代码大小。
我们将使用
–closure 1
选项与–O3/s
Emscripten 优化选项一起优化 WebAssembly 二进制文件:$ time emcc -O3 --closure 1 optimization_check.c emcc -O3 --closure 1 optimization_check.c 2.40s user 0.42s system 105% cpu 2.681 total
生成的文件大小如下:
$ l 324B optimization_check.c 1.8K a.out.wasm 6.5K a.out.js
与
emcc –O3
一起,我们传递–closure 1
来进一步优化生成的文件。闭包编译器将 JavaScript 文件大小与emcc -O3
选项相比减少了 50%,编译用时为 2.681 秒:time emcc -Os --closure 1 optimization_check.c emcc -Os --closure 1 optimization_check.c 2.53s user 0.42s system 106% cpu 2.778 total
让我们列出当前文件夹中的文件,以检查生成的文件及其大小:
$ l 324B optimization_check.c 1.7K a.out.wasm 6.5K a.out.js
与
emcc –Os
一起,我们传递–closure 1
来进一步优化生成的二进制文件。闭包编译器在emcc -Os
选项下将.wasm
文件大小减小了一点,编译用时为 2.778 秒。注意
在为大小优化时,尝试同时使用
-O3
或-Os
与--closure 1
来优化 JavaScript 和 WebAssembly 模块。有关各种选项和标志的更多信息,请访问 https://emscripten.org/docs/tools_reference/emcc.htmlhttps://clang.llvm.org/docs/CommandGuide/clang.html。
有关可用优化选项的更多信息,请访问 https://docs.microsoft.com/en-us/cpp/build/reference/o-options-optimize-code?view=vs-2017。
了解更多关于闭包编译器的信息,请访问 https://developers.google.com/closure/compiler。
了解更多关于使用 Emscripten 优化大型代码库的信息,请访问 https://emscripten.org/docs/optimizing/Optimizing-Code.html#very-large-codebases。
总结
在本章中,我们学习了如何安装和使用 Emscripten 将 C/C++ 编译成 WebAssembly 模块。我们还探讨了 emsdk 工具和在生成 WebAssembly 模块时的各种优化级别。在下一章中,我们将探索 WebAssembly 模块。
-