为什么asm.js会导致性能下降?

40

为了测试它的性能,我手写了一个非常简短的asm.js模块,使用32位整数数学和类型化数组(Int32Array)模拟2D波动方程。我有三个版本,尽可能相似:

  1. 普通的JavaScript(即易读的C风格)
  2. 与1相同,在其上添加了asm.js注释,使其可以通过Firefox和其他工具的验证器
  3. 与2相同,但是在顶部没有“use asm”;指令

我在http://jsfiddle.net/jtiscione/xj0x0qk3/上留下了一个演示,让您可以切换模块以查看使用每个模块的效果。所有三个版本都可以正常工作,但速度不同。这是热点(带有asm.js注释):

for (i = 0; ~~i < ~~h; i = (1 + i)|0) {
    for (j = 0; ~~j < ~~w; j = (1 + j)|0) {
        if (~~i == 0) {
            index = (1 + index) | 0;
            continue;
        }
        if (~~(i + 1) == ~~h) {
            index = (1 + index) | 0;
            continue;
        }
        if (~~j == 0) {
            index = (1 + index) | 0;
            continue;
        }
        if (~~(j + 1) == ~~w) {
            index = (1 + index) | 0;
            continue;
        }
        uCen = signedHeap  [((u0_offset + index) << 2) >> 2] | 0;
        uNorth = signedHeap[((u0_offset + index - w) << 2) >> 2] | 0;
        uSouth = signedHeap[((u0_offset + index + w) << 2) >> 2] | 0;
        uWest = signedHeap [((u0_offset + index - 1) << 2) >> 2] | 0;
        uEast = signedHeap [((u0_offset + index + 1) << 2) >> 2] | 0;
        uxx = (((uWest + uEast) >> 1) - uCen) | 0;
        uyy = (((uNorth + uSouth) >> 1) - uCen) | 0;
        vel = signedHeap[((vel_offset + index) << 2) >> 2] | 0;
        vel = vel + (uxx >> 1) | 0;
        vel = applyCap(vel) | 0;
        vel = vel + (uyy >> 1) | 0;
        vel = applyCap(vel) | 0;
        force = signedHeap[((force_offset + index) << 2) >> 2] | 0;
        signedHeap[((u1_offset + index) << 2) >> 2] = applyCap(((applyCap((uCen + vel) | 0) | 0) + force) | 0) | 0;
        force = force - (force >> forceDampingBitShift) | 0;
        signedHeap[((force_offset + index) << 2) >> 2] = force;
        vel = vel - (vel >> velocityDampingBitShift) | 0;
        signedHeap[((vel_offset + index) << 2) >> 2] = vel;
        index = (index + 1)|0;
    }
}

“普通JavaScript”版本的结构如上所述,但不包括asm.js需要的位运算符(例如“x|0”,“~~x”,“arr[(x<<2)>>2]”等)。

以下是我在使用Firefox(Developer Edition v. 41)和Chrome(版本44)时对所有三个模块的测试结果,以每次迭代的毫秒数计:

  • FIREFOX(版本41):20毫秒、35毫秒、60毫秒。
  • CHROME(版本44):25毫秒、150毫秒、75毫秒。

因此,在两个浏览器中,“普通JavaScript”都获胜。asm.js所需的注释存在会使性能下降三倍。此外,“use asm”;指令的存在明显有影响-它能帮助Firefox一点,并使Chrome崩溃!

仅仅添加位运算符应该不会引入三倍的性能退化,而且告诉浏览器使用asm.js也只会在Firefox中有轻微的帮助,而在Chrome中完全反效果,这似乎很奇怪。


首先,我在Chrome 44和FF 39(Win XP,32位)中运行了“ Massive”基准测试,并在这里提供了我的ChromeFirefox结果(复制并转储到基准测试页面上的“输入从另一个运行中复制的数据”字段 - 是的,它可以使用实际的HTML)。除了一个点(“poppler-cold-preparation”)外,Chrome在所有地方都比较慢,在最极端的情况下比FF慢24.6倍。看起来Chrome目前无法合理处理asm.js。 - Siguza
2
你有没有“基准测试”过后续/重复调用的情况,因为汇编在编译/优化阶段会使用更多时间(我猜测)? - birdspider
@birdspider 你的意思是多次运行基准测试吗?不,我只是采用了现有的内容...当前界面似乎需要重新加载页面才能再次运行基准测试,很可能需要重新编译/优化代码。但整个基准测试对我来说大约需要15分钟才能完成,所以我认为编译时间并不是很重要的因素。如果Chrome真的需要这么长时间来编译,那么即使代码得到运行也让我感到困惑。 - Siguza
2
@birdspider 那为什么加上/去掉'use asm'会有很大的差异呢?这就不合理了... - Siguza
2个回答

9
实际上,asm.js并不是用于手动编写代码,而只是其他语言的编译结果。据我所知,没有工具可以验证asm.js代码。您是否尝试使用C语言编写代码,并使用Emscripten生成asm.js代码?我强烈怀疑结果会有很大不同,并且针对asm.js进行了优化。
我认为混合使用类型和非类型变量只会增加复杂性而没有任何好处。相反,“asm.js”代码更加复杂:我尝试解析了asm.js和普通函数在jointjs.com/demos/javascript-ast上的结果如下:
- 普通js函数有137个节点和746个标记 - asm.js函数有235个节点和1252个标记
如果每个循环中有更多指令需要执行,那么它很容易变得更慢。

3
虽然你对它不是为手写而设计是正确的,但是在这里似乎有一个 asm.js 验证器,并且 OP 的 AsmWaveModule 通过了检查。 - Siguza
此外,Firefox 在控制台中打印出以下内容:成功编译 asm.js 代码(总编译时间为 1 毫秒;未存储在缓存中(太小而无法受益))。当我从 OP 的 jsfiddle 中删除一个 ~~ 时,这就变成了 TypeError: asm.js type error: Disabled by debugger。因此,似乎 asm.js 本身并没有问题。 - Siguza
我同意,节点/令牌增加50%应该会减慢速度,但这是一个令人惊讶的打击。注释(包括“no asm”)在Firefox和Chrome上引入了3倍的减速。我在Safari上尝试过(不支持asm.js),所有三个版本都非常慢(不足为奇),但注释只会使Safari减速50%。 - jtiscione
2
我刚在IE上尝试了一下(即使没有asm.js支持)。普通版本运行良好,但是无论是否使用指令,注释的存在都会导致IE 崩溃。这与asm.js无关,但真的很奇怪。 - jtiscione
1
@jtiscione 不,我敢打赌这是IE的预期行为。开玩笑的,伪汇编版本变慢是有道理的,因为需要运行更多的指令,但是仅仅因为存在 'use asm'; 就使一切变慢三倍,这不能简单地归结为“它不是为手写设计的”。 - Siguza
我尝试使用FF对代码进行分析。 这种方式下Iterate函数的结果如下: NoAsm: 1.532 Asm: 0.807 AsmNoDir: 1.196 这些结果对我来说更有意义,但我看了你的代码,我认为你衡量性能的方法是正确的。所以说,老实说,我现在很困惑。 - cristian v

1

切换 asm.js 上下文存在某些固定成本。理想情况下,您只需切换一次并将所有代码作为 asm.js 运行在应用内。然后,您可以使用类型化数组控制内存管理,并避免大量垃圾回收。我建议重新编写分析器,并在 asm.js 中进行 asm.js 测量,无需切换上下文。


但在这种情况下,没有垃圾需要收集,因为所有操作都是在启动时实例化的共享ArrayBuffer堆上完成,并通过类型化数组视图(Int32Array和Uint32Array)访问。据我所知,没有办法将整个应用程序放入asm.js中。您始终需要外部代码来实例化编译的模块并调用其入口点。 - jtiscione
我尝试在iterate函数内部测量时间,但似乎没有什么区别...或者使用stdlib.performance.now()会调用另一个上下文切换吗?如果是这样,那么是否有可能测量asm.js函数实际花费的时间? - Siguza
如果你查看JS源代码,总是将“totalCycles”初始化为4,并将其提高到8或12,它将通过上面的热点代码循环2倍和3倍。在Chrome上,普通JS的减速从22到40到65毫秒,asm.js从140到275到400毫秒,没有指令的asm.js从70到140到210毫秒。由于运行时间几乎与它在上述代码中花费的时间成正比,我认为上下文切换的开销看起来相当小,至少在这种情况下。 - jtiscione

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接