【问题标题】:Why does asm.js deteriorate performance?为什么 asm.js 会降低性能?
【发布时间】:2015-08-01 23:09:15
【问题描述】:

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

  1. 普通(即易读,尽管是 C 风格)JavaScript
  2. 与 1 相同,添加了 asm.js 注释,以便根据 Firefox 和其他工具通过验证器
  3. 与 2 相同,但没有“使用 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 ]”等)

这些是我机器上所有三个模块的结果,使用 Firefox(开发者版 v. 41)和 Chrome(版本 44),每次迭代以毫秒为单位:

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

所以普通的 JavaScript 在两种浏览器中都胜出。 asm.js 所需注释的存在使两者的性能下降了 3 倍。此外,“使用 asm”的存在;指令有一个明显的效果——它对 Firefox 有点帮助,让 Chrome 屈服!

仅添加按位运算符会引入三倍的性能下降,这似乎很奇怪,而这无法通过告诉浏览器使用 asm.js 来克服。另外,为什么告诉浏览器使用 asm.js 对 Firefox 的帮助很小,而在 Chrome 中却完全适得其反?

【问题讨论】:

  • 首先,我在 Chrome 44 和 FF 39(Win XP,32 位)中运行了 "Massive" benchmark,这是我对 ChromeFirefox 的结果(复制并转储到“在基准页面上输入从另一个运行复制的数据”字段 - 是的,它适用于实际的 HTML)。除了一点(“poppler-cold-preparation”),Chrome 到处都慢,在最极端的情况下比 FF 慢 24.6 倍。看起来 Chrome 目前只是无法合理处理 asm.js。
  • 只是一个想法,您是否对后续/重复调用进行了“基准测试”,因为 asm 在编译/选择阶段会使用更多时间(我想)?
  • @birdspider 你的意思是多次运行基准测试?不,我只是拿走了那里的东西......当前界面似乎需要重新加载页面才能再次运行基准测试,很可能需要再次编译/优化代码。但是整个基准测试对我来说大约需要 15 分钟才能完成,所以我认为编译时间并不是一个很大的因素。如果 Chrome 真的需要这么长时间来编译,我会感到困惑的是,代码甚至可以运行根本
  • 我认为它根本没有实现 (dev.modern.ie/platform/status/asmjs) asm.js;谷歌浏览 chrome 论坛,它处于某种测试阶段,名称为 turbofan - 也是 (phoronix.com/…)
  • @birdspider 那么为什么在添加/删除'use asm' 时会有很大的不同呢?它只是没有加起来......

标签: javascript performance bitwise-operators asm.js


【解决方案1】:

实际上 asm.js 并不是为了手工编写代码而创建的,而只是作为从其他语言编译而来的结果。据我所知,没有工具可以验证 asm.js 代码。 您是否尝试过用 C 语言编写代码并使用 Emscripten 生成 asm.js 代码?我强烈怀疑结果会完全不同,并且针对 asm.js 进行了优化。

我认为混合有类型和无类型的变量只会增加复杂性而没有任何好处。相反,“asm.js”代码更复杂:我尝试解析 jointjs.com/demos/javascript-ast 上的 asm.js 和普通函数,结果是:

  • 纯js函数有137个节点和746个token
  • asm.js 函数有 235 个节点和 1252 个令牌

我会说,如果您在每个循环中执行更多指令,它很容易就会变慢。

【讨论】:

  • 虽然你说得对,它不是为手写而设计的,但 here 似乎是一个 asm.js 验证器,并且 OP 的 AsmWaveModule 通过了检查。
  • 此外,Firefox 在控制台中打印:Successfully compiled asm.js code (total compilation time 1ms; not stored in cache (too small to benefit))。当我从 OP 的 jsfiddle 中删除 ~~ 时,它会变为 TypeError: asm.js type error: Disabled by debugger。所以看起来 asm.js 本身没有问题。
  • 我同意增加 50% 的节点/令牌应该会减慢它的速度,但这是一个令人惊讶的打击。注释(包括“无 asm”;包括)在 Firefox 和 Chrome 上引入了 3 倍的减速。我在 Safari 上尝试过(不支持 asm.js),所有 3 个版本都非常慢(不足为奇),但注释只产生Safari 速度减慢 50%。
  • 我刚刚在 IE 上尝试过(也没有 asm.js 支持)。普通版本运行良好,但不管有没有该指令,仅仅存在注释就会使 IE crash。那和asm.js没有关系,但是真的很奇怪。
  • @jtiscione 不,我敢打赌这是 IE 中的预期行为。撇开玩笑不谈,伪 asm 版本更慢是有道理的,这仅仅是因为要运行的指令更多,但仅仅存在 'use asm'; 就可以将所有速度降低 3 倍不会简化为“它不是为手写而设计的”。
【解决方案2】:

切换 asm.js 上下文需要一些修复成本。理想情况下,您只需执行一次,然后在您的应用程序中以 asm.js 的形式运行所有代码。然后,您可以使用类型化数组控制内存管理并避免大量垃圾收集。我建议重写分析器并在 asm.js 中测量 asm.js - 无需上下文切换。

【讨论】:

  • 但是在这种情况下没有垃圾被收集,因为所有操作都在一个共享的 ArrayBuffer 堆上完成,该堆在启动时实例化并通过类型化数组视图(Int32Array 和 Uint32Array)进行访问。 AFAIK 没有办法在 asm.js 中容纳整个应用程序。您将始终需要外部代码来实例化已编译的模块并调用其入口点。
  • I tried to measure the time from inside iterate as well,但它似乎没有什么区别......或者使用stdlib.performance.now() 调用另一个上下文切换?如果是这样,是否甚至可以测量在 asm.js 函数中花费的实际时间?
  • 如果你浏览 JS 源代码,它总是将“totalCycles”初始化为 4,并将其提高到 8 或 12,它将通过上面的热点代码循环 2X 和 3X 次。在 Chrome 上,普通 JS 的减速时间从 22 到 40 到 65 毫秒,asm.js 的减速时间从 140 到 275 到 400 毫秒,没有指令的 asm.js 的减速时间从 70 到 140 到 210 毫秒。由于运行所需的时间几乎与在上面的代码中花费的时间成正比,我认为上下文切换的开销看起来很小,至少在这种情况下是这样。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-08-30
  • 1970-01-01
  • 2016-08-08
  • 2021-04-19
  • 2016-02-25
  • 1970-01-01
  • 2012-03-08
相关资源
最近更新 更多