优化代码

通常,你应该先编译并运行你的代码,而不进行优化,这是当你只运行 emcc 而不指定优化级别时的默认行为。这种未优化的构建包含一些检查和断言,这些检查和断言可以帮助你确保代码正常运行。一旦代码正常运行,强烈建议你优化你发布的构建,原因有几个:首先,优化的构建体积更小、速度更快,因此它们加载速度更快、运行更流畅,其次,**未**优化的构建包含调试信息,例如文件和函数的名称、JavaScript 中的代码注释等(除了增加大小之外,还可能包含你不希望发布给用户的内容)。

本页的其余部分将介绍如何优化你的代码。

如何优化代码

通过在运行 emcc 时指定 优化标志 来优化代码。级别包括:-O0(无优化)、-O1-O2-Os-Oz-Og 以及 -O3

例如,要使用优化级别 -O2 编译

emcc -O2 file.cpp

较高的优化级别引入了越来越激进的优化,从而在提高性能和代码大小的同时,增加了编译时间。这些级别还可以突出显示代码中与未定义行为相关的不同问题。

你应该使用的优化级别主要取决于当前的开发阶段

  • 首次移植代码时,使用默认设置(无优化)在你的代码上运行emcc。检查你的代码是否正常工作,并 调试 并修复任何问题,然后继续。

  • 在开发期间,使用较低的优化级别构建,以缩短编译/测试迭代周期(-O0-O1)。

  • 使用 -O2 构建以获得经过良好优化的构建。

  • 使用 -O3-Os 构建可以产生比 -O2 更好的构建,对于发布构建,值得考虑。-O3 构建比 -O2 更加优化,但代价是编译时间明显增加,代码大小可能会更大。-Os 在增加编译时间方面类似,但它专注于减少代码大小,同时进行额外的优化。值得尝试这些不同的优化选项,以查看什么最适合你的应用程序。

  • 以下部分将介绍其他优化方法。

注意

  • emcc 优化标志(-O1, -O2 等)的含义与gccclang 和其他编译器类似,但也存在差异,因为优化 WebAssembly 包含一些额外的优化类型。emcc 级别映射到 LLVM 位码优化级别的文档在参考中列出。

Emscripten 如何优化

将源文件编译为目标文件的工作原理与你预期使用 clang 和 LLVM 的原生构建系统相同。当链接目标文件到最终可执行文件时,Emscripten 会根据优化级别执行额外的优化

  • 运行 Binaryen 优化器。Binaryen 对 Wasm 执行通用优化,LLVM 无法执行,还执行一些全局程序优化。(请注意,Binaryen 的全局程序优化可能会执行诸如内联之类的操作,这在某些情况下可能会令人惊讶,因为 LLVM IR 属性(如 noinline)在此时已丢失。)

  • 在此阶段生成 JavaScript,并由 Emscripten 的 JS 优化器进行优化。你还可以选择运行 Closure 编译器,强烈建议你使用 Closure 编译器来减小代码大小。

  • Emscripten 还通过最小化 Wasm+JS 之间的导入和导出,并通过运行元数据 DCE 来优化组合的 Wasm+JS,元数据 DCE 会删除跨越两个世界的循环中未使用的代码。

高级编译器设置

你可以 传递给编译器 几个标志,以影响代码生成,这也将影响性能,例如 DISABLE_EXCEPTION_CATCHING。这些标志在 src/settings.js 中有文档记录。

WebAssembly

Emscripten 默认会发出 WebAssembly。你可以使用 -sWASM=0(在这种情况下,emscripten 将发出 JavaScript)关闭此功能,如果你希望输出在尚未支持 Wasm 的地方运行,则需要使用此功能,但缺点是代码更大、速度更慢。

代码大小

本部分介绍与代码大小相关的优化和问题。它们对于你希望实现最小占用空间的小型项目或库很有用,对于大型项目也同样有用,因为它们庞大的体积可能会导致你希望避免的各种问题(例如启动速度慢)。

权衡代码大小和性能

你可能希望使用 -Os-Oz 构建项目中性能要求不高的源文件,并将剩余部分使用 -O2 构建(-Os-Oz-O2 类似,但它们会以性能为代价来减少代码大小。-Oz-Os 更能减少代码大小。)

另外,你可以使用 -Os-Oz 执行最终的链接/构建命令,以使编译器在生成 WebAssembly 模块时更专注于代码大小。

其他代码大小提示

除了上述方法之外,以下提示还有助于减小代码大小

  • 对未编译的代码使用 closure 编译器--closure 1。这可以极大地减小支持 JavaScript 代码的大小,强烈推荐。但是,如果您添加了自己的额外 JavaScript 代码(例如,在 --pre-js 中),则需要确保它 正确使用 closure 注释

  • Floh 关于此主题的博客文章 非常有用。

  • 确保在您的 Web 服务器上使用 gzip 压缩,所有浏览器现在都支持。

以下编译器设置可以提供帮助(有关更多详细信息,请参阅 src/settings.js

  • 尽可能禁用内联,使用 -sINLINING_LIMIT。使用 -Os 或 -Oz 编译通常会避免内联。(内联可以使代码更快,因此请谨慎使用。)

  • 您可以使用 -sFILESYSTEM=0 选项来禁用文件系统支持代码的捆绑(如果未使用,编译器应该优化它,但可能并非总是成功)。例如,如果您正在构建纯计算库,这将很有用。

  • ENVIRONMENT 标志允许您指定输出仅在 Web 上运行,或仅在 node.js 上运行等。这可以防止编译器发出代码以支持所有可能的运行时环境,从而节省约 2KB。

LTO

链接时优化 (LTO) 允许编译器执行更多优化,因为它可以跨单独的编译单元进行内联,甚至与系统库一起进行内联。LTO 通过使用 -flto 编译对象文件来启用。此标志的效果是发出 LTO 对象文件(从技术上讲,这意味着发出位代码)。链接器可以处理混合的 Wasm 对象文件和 LTO 对象文件。在链接时传递 -flto 还会触发使用 LTO 系统库。

因此,要允许 LLVM Wasm 后端具有最大的 LTO 机会,请使用 -flto 构建所有源文件,并使用 flto 进行链接。

EVAL_CTORS

使用 -sEVAL_CTORS 构建将在编译时评估尽可能多的代码。这包括“全局构造函数”函数(LLVM 发出的在 main() 之前运行的函数)以及 main() 本身。可以评估的尽可能多,然后将结果状态“快照”到 wasm 中。然后,当程序运行时,它将从该状态开始,并且不需要执行该代码,这可以节省时间。

此优化可以减少或增加代码大小。例如,如果一小段代码在内存中产生了大量更改,那么总大小可能会增加。最好使用此标志进行构建,然后衡量代码和启动速度,看看在您的程序中权衡是否值得。

您可以努力编写 EVAL_CTORS 友好的代码,通过尽可能延迟无法评估的内容。例如,对导入的调用会阻止这种优化,因此,如果您有一个创建 GL 上下文然后进行一些纯计算以在内存中设置不相关数据结构的游戏引擎,那么您可以颠倒该顺序。然后,纯计算可以首先运行并被评估掉,而对导入的 GL 上下文创建调用不会阻止它。您可以做的其他事情是避免使用 argc/argv,避免使用 getenv(),等等。

使用此选项时会显示日志,以便您可以查看是否可以改进。以下是一些 emcc -sEVAL_CTORS 输出的示例

trying to eval __wasm_call_ctors
  ...partial evalling successful, but stopping since could not eval: call import: wasi_snapshot_preview1.environ_sizes_get
       recommendation: consider --ignore-external-input
  ...stopping

第一行指示尝试评估 LLVM 的运行全局构造函数的函数。它评估了该函数的一部分,但随后它在 WASI 导入 environ_sizes_get 上停止,这意味着它正在尝试从环境中读取。正如输出所说,您可以告诉 EVAL_CTORS 忽略外部输入,这将忽略此类内容。您可以使用模式 2 启用它,即使用 emcc -sEVAL_CTORS=2 进行构建

trying to eval __wasm_call_ctors
  ...success on __wasm_call_ctors.
trying to eval main
  ...stopping (in block) since could not eval: call import: wasi_snapshot_preview1.fd_write
  ...stopping

现在它已成功完全评估 __wasm_call_ctors。然后它继续进行 main,在那里它由于对 WASI 的 fd_write 的调用而停止,即对打印某物的调用。

非常大的代码库

上一节关于减少代码大小的内容可能对非常大的代码库有所帮助。此外,以下是一些可能会有用的其他主题。

独立运行

如果您在浏览器中遇到内存限制,那么让您的项目独立运行可能会有所帮助,而不是放在包含其他内容的网页中。如果您打开一个新的网页(作为新标签或新窗口),其中只包含您的项目,那么您将最大限度地避免内存碎片问题。

模块拆分

如果您的模块足够大,以至于下载和实例化它所花费的时间明显影响了应用程序的启动性能,那么可能值得拆分模块并延迟加载启动应用程序不需要的代码。有关如何执行此操作的指南,请参阅 模块拆分请注意,模块拆分是一个实验性功能,可能会发生变化。

其他优化问题

C++ 异常

默认情况下,在 -O1(及以上)中,捕获 C++ 异常(具体来说,是发出 catch 块)是禁用的。由于 WebAssembly 当前如何实现异常,这使得代码更小更快(最终,Wasm 应该获得对异常的本机支持,并且不会出现此问题)。

要重新启用优化代码中的异常,请使用 -sDISABLE_EXCEPTION_CATCHING=0 运行emcc(请参阅 src/settings.js)。

注意

当禁用异常捕获时,抛出的异常将终止应用程序。换句话说,异常仍然会被抛出,但不会被捕获。

注意

即使没有发出 catch 块,除非您使用 -fno-exceptions 构建源文件,否则会有一些代码大小开销,这将省略所有异常支持代码(例如,它将避免在 std::vector 中的错误中创建正确的 C++ 异常对象,并且只是在它们发生时中止应用程序)

C++ RTTI

C++ 运行时类型信息支持(动态转换等)会增加有时不需要的开销。例如,在 Box2D 中,既不需要 rtti 也不需要异常,如果您使用 -fno-rtti -fno-exceptions 构建源文件,那么它会将输出缩小 15%(!)。

内存增长

使用 -sALLOW_MEMORY_GROWTH 进行构建允许根据应用程序的需求更改使用的总内存量。这对不知道事先需要多少内存的应用程序很有用。

查看代码优化过程

启用 调试模式(EMCC_DEBUG) 以输出每个编译阶段的文件,包括主要优化操作。

分配

使用的默认 malloc/free 实现是 dlmalloc。您还可以选择 emmalloc (-sMALLOC=emmalloc),它更小但速度较慢,或者 mimalloc (-sMALLOC=mimalloc),它更大,但在多线程应用程序中扩展得更好,在该应用程序中,对 malloc/free 有争用(请参阅 分配器性能)。

不安全的优化

您可能想尝试的一些不安全优化是

  • --closure 1:这有助于减小未生成的(支持/粘合)JS 代码的大小,以及启动速度。但是,如果您没有进行适当的 Closure 编译器 注释和导出,它可能会中断。但这值得一试!

分析

现代浏览器具有 JavaScript 分析器,可以帮助找到代码中较慢的部分。由于每个浏览器的分析器都有局限性,因此强烈建议在多个浏览器中进行分析。

为了确保编译后的代码包含足够的信息进行分析,请使用 分析 以及优化和其他标志构建您的项目

emcc -O2 --profiling file.cpp

解决性能不佳问题

Emscripten 编译的代码通常可以接近本机构建的速度。如果性能明显低于预期,您还可以执行以下其他故障排除步骤

  • 构建项目 是一个两阶段过程:将源代码文件编译成 LLVM 以及从 LLVM 生成 JavaScript。您是否在两个步骤中都使用相同的优化值进行构建 (-O2-O3)?

  • 在多个浏览器上进行测试。如果性能在一个浏览器上可以接受,而在另一个浏览器上明显更差,那么请 提交错误报告,并注意有问题的浏览器和其他相关信息。