emscripten使用教程之优化你的代码

翻译:云荒杯倾

译文地址

通常来说,你只要编译并运行你的代码,并不需要优化。一旦你能保证代码运行正确,
你就可以使用本文提到的技术让你的代码加载和运行的更快。

1、怎么优化代码


在使用emcc时,通过指定优化标志来进行优化。优化有级别之分,分别是:-O0,-O1,
-O2,-Os,-Oz,-O3。

比如,下面代码就是使用-O2级别进行优化编译

1
emcc -O2 file.cpp

随着优化级别增加,渐进引入更加激进的优化方式,从而导致更高的性能和更大的代码体积
,代价是编译时间会增加。优化项也会引发一些问题。

那么,什么时候用什么级别的优化呢?

  • 第一次移植代码时,运行emcc使用默认settings,不要优化。检查你的代码是否工作
    并修复问题。
  • 开发中为了一个较短的编译迭代周期,使用低级别的优化。即-O0或者-O1。
  • 发布代码时,使用-O2或者-O3。-O3比-O2更加优化,不过需要明显更长的编译时间。
  • 其他优化在下面部分讨论。
    除了-Ox选项或级别对所有优化项目是共同的,js优化、llvm优化、llvm link-time优化由各自独立的编译选项来控制。
    1
    2
    3
    Note:
    emcc优化标志的意义不同于gcc,clang,或其他编译器的相似名字的选项,因为对JS代码优化和对原生代码优化是非常不同的。
    emcc的优化级别和llvm bitcode优化级别的映射关系写在reference那篇文章中。

2、高级编译器设置


你可以给编译器传一些标志进去以影响代码的生成,同时这也影响性能。比如DISABLE_EXCEPTION_CATCHING,
这些可以在settings 看到。

一些有用的标志是:

  • NO_EXIT_RUNTIME: 编译时使用-s NO_EXIT_RUNTIME=1 ,这样编译器就知道你不想让
    程序在运行完main()函数后就结束。此时,编译器就会放弃atexit和全局析构这两个
    函数的调用(如果你不设置-s NO_EXIT_RUNTIME=1,是会调用的),这样就减小了代码体积,
    并且加快了启动速度。
    当你的main()函数完成但是你仍想要执行代码时,这是有用的,比如在app中使用了主循环。
    1
    2
    3
    4
    NOTE:
    如果你的C代码里面有emscripten_set_main_loop(),它被emscripten检测到的话,emscripten也不会
    在main结束调用后关闭运行时,但是呢,你最好还是在编译选项中加-s NO_EXIT_RUNTIME=1吧,
    毕竟这样可以减小生成不必要的代码,是不是?!

3、代码大小


本节将描述与代码大小相关的优化和问题。它们对大小项目都很有用。
在小项目或库中,您可以获得最小的封装;在大型项目中,
代码的巨大规模可能会导致您本来想要避免的问题(比如慢速启动速度)。

3.1 内存初始化

默认情况,emscripten会在.js文件中写静态内存初始化代码。这会导致js文件很大,
也会减缓启动速度。由于js引擎对数组大小的限制,这也可能导致js引擎抛出 array initializer too large
Too much recursion 两个报错。
emcc –memory-init-file 1的设置会将静态内存代码初始化的这一部分代码放到.mem后缀
的文件中,和.js文件分开。这个.mem文件会在main()函数被调用,代码执行前异步加载。

1
2
NOTE:
从Emscripten 1.21.1 起,-O2及以上的优化级别默认启动了这项设置。

3.2 代码大小和性能的平衡

你可能希望在项目中编译性能不用怎么好的源文件,那就使用-Os和-Oz吧。其他部分
使用-O2。(-Os和-Oz以性能为代码减小代码体积,他们和-O2相似)。
注意:-Oz编译比较耗时。

3.3 其他关于代码大小的小贴士(杂项)

除了3.1和3.2,下面的建议也可以减小代码大小:

  • 这篇文章 很有帮助
  • 从bitcode到js的编译过程使用 llvm-lto,用法:–llvm-lto 1。
  • 不允许嵌入/内联代码:-s INLINING_LIMIT=1。
  • 下面还有6条,不逐一翻译。

4、大型代码库优化


上面第三部分介绍的减小代码体积部分对大型代码库很有用,另外,还有一些其他对
大型代码库优化有用的主题。

4.1 分离asm.js文件避免内存峰值

默认情况下,Emscripten会编译出一个JS文件,包含整个代码库:一个是asm.js代码,
一个是运行环境、连接浏览器等胶水代码。对于一个非常大的代码库,这样搞
内存使用方面会效率低下,因为所有的代码都在一个脚本就意味着js引擎可能使用一些内存
来解析和编译asm,而且在开始运行代码库之前,这部分内存可能不会释放。在大型游戏中,
开始运行代码可能需要动态分配一个很大的类型数组作内存,因此您可能会看到内存的“峰值”,
在此之后将释放临时的编译内存(解析和编译asm那部分内存)。如果足够大,这个峰值
会导致浏览器耗尽内存,无法加载应用程序。这是Chrome的一个已知问题(其他浏览器似乎没有这个问题)。

一种方法是分离出asm。js到另一个文件,并确保浏览器在编译asm之间有一个事件循环
,保证编译asm和运行程序有一个顺序。这可以通过运行emcc –separate-asm来实现。

4.2 自运行

4.3 列提纲outlining

JavaScript引擎通常会慢慢地编译非常大的函数(相对于它们的大小),
并不能有效地(或根本)对它们进行优化。解决这个问题的方法之一是使用“列提纲”:
将它们分解成更小的函数,可以更有效地编译和优化。
“列提纲”增加了总体代码的大小,并且可以使一些代码变得不那么优化。尽管如此,
“列提纲”有时可以提高启动速度和运行时速度。
了解更多信息

OUTLINING_LIMIT选项定义了emscripten是否会分解一个函数为众多小函数的值。
怎么选一个合适的大小值进行拆解,怎么决定哪些函数被拆解,
点这里

4.4 激进地消除变量

激进的变量消除尝试尽可能地去删除变量,甚至愿意付出重复表达式这种方法
(会增加代码大小)的代价。如果你有非常大的函数,这可以提高速度。
例如,它可以使sqlite(它有一个巨大的解释器循环,有数千行)快7%。

5、其他优化问题


5.1 C++异常

在- o1(及以上)中默认关闭c++异常。
这防止了try - catch块的生成,它使代码运行得更快,也使得代码更小。

要在优化的代码中重新启用异常,请运行emcc命令- s DISABLE_EXCEPTION_CATCHING= 0

5.2 内存增长

使用- s ALLOW_MEMORY_GROWTH= 1命令的编译允许根据应用程序的需求改变内存总量。
这对于预先不知道需要多少内存的应用程序很有用,但是它禁用了一些优化。(目前正在进行改进。)

5.3 内联/嵌套

内联通常产生较大的函数,因为这些可以使编译器的优化更加有效。
不幸的是,大型函数在运行时比多个较小的函数要慢,因为JavaScript引擎通常
不优化大函数(因为害怕长时间JIT),或者优化它们导致明显的停顿。

1
2
NOTE:
- o1和- o2默认会内联函数。讽刺的是,在某些情况下,这实际上会降低性能!

5.4 查看“代码优化遍”记录

启用调试模式(EMCC_DEBUG)将每一次JavaScript优化遍输出到文件。

6、不安全的优化


你可能想试一些不安全的优化项:

  • -s FORCE_ALIGNED_MEMORY=1: 使所有内存存取完全对齐。这会破坏那些实际上
    不需要内存对齐的代码。
  • –llvm-lto 1:可以使llvm link-time得到优化,有些情况下有用。但是他们有一些已知的
    问题,所以代码必须被充分地测试。点击查看更多
  • –closure 1: 可以减小胶水代码的体积,减少启动时间。但是如果你没有做
    正确的closure complier,可能有问题。

7、性能分析


现代浏览器有JavaScript profiler,可以帮助查找代码中较慢的部分。
由于每个浏览器的分析器都有限制,所以建议在多个浏览器中进行分析。
为了确保已编译的代码包含足够的信息进行分析,
可以使用下面标志设定编译命令:

1
emcc -O2 --profiling file.cpp

8、排查不佳的性能


emscrip10编译的代码目前可以实现本地构建的大约一半的速度。
如果性能明显低于预期,您还可以运行下面的其他故障排除步骤:

  • 构建项目是一个两阶段的过程:将源代码文件编译成LLVM,并从LLVM生成JavaScript。
    您是否在两个步骤中使用相同的优化值(- o2或- o3)?
  • 在多个浏览器上进行测试。如果在一个浏览器上性能可以接受,在另一个浏览器上
    明显较差,那么可以给我们提交一个bug报告,注意写清楚出问题的浏览器和其他相关信息。
  • 代码是否在Firefox中验证正确(在火狐控制台查看是否有:“成功编译asm.js代码”信息输出)。
    如果您在使用最新版本的Firefox和Emscripten时看到验证错误,请提交一个错误报告。