【问题标题】:TypedArray Set vs. Unrolled Loop (Javascript)TypedArray 集合与展开循环 (Javascript)
【发布时间】:2015-05-24 06:33:59
【问题描述】:

在尝试为自己构建 Webgl 3d 库时(主要是为了学习),我遵循了从各种来源找到的文档,其中指出 TypedArray 函数 set()(特别是针对 Float32Array)应该是“as fast as" memcpy in C(显然是舌头在脸颊上),根据html5rocks,实际上是最快的。看起来是正确的(在 javascript 中没有设置循环,消失在一些超快的类型数组中,纯 C 废话等等)。

我在glMatrix 看了一眼(顺便说一句,做得很好!),并注意到他(作者)说他为了速度而展开了所有循环。这显然是 javascript 大师通常会以尽可能快的速度做的事情,但是,根据我之前的阅读,我认为我在这个库上有 1-up,具体来说,他创建了他的库以同时使用数组和类型数组,因此我认为使用“set()”可以提高速度,因为我只对类型化数组类型感兴趣。

为了测试我的理论,我设置了这个jsperf。不仅 set() 相对缺乏速度,而且我尝试过的所有其他技术(在 jsperf 中)都胜过它。这是迄今为止最慢的。

最后,我的问题是:为什么?从理论上讲,我可以理解在 spidermonkey 或 chrome V8 js 引擎中进行高度优化的循环展开,但是输给 for 循环似乎很荒谬(jsperf 中的 copy2),尤其是如果它的目的是由于原始连续而在理论上加快复制速度内存数据类型(typedarrays)。无论哪种方式,都感觉 set() 函数坏了。

这是我的代码吗?我的浏览器? (我使用的是 Firefox 24)还是我错过了其他一些优化理论?任何有助于理解这个与我的想法和理解相反的结果都会非常有帮助。

【问题讨论】:

  • 我能想到的唯一解释是它不假设数组类型匹配,所以做一些可怕的不必要的类型转换。虽然这是一个猜测,但很可能是别的东西!
  • 确实 set() 函数可以接收典型的 javascript 数组或 TypedArray ......也许吧?但它似乎仍然很糟糕......它甚至没有接近其他类型的在数组中复制数据的方法......

标签: javascript performance optimization webgl typed-arrays


【解决方案1】:

这是一个老问题,但如果您特别需要优化一些性能不佳的代码,则有理由使用TypedArrays。关于 JavaScript 中的 TypedArray 对象,需要了解的重要一点是它们是 views,它们表示 ArrayBuffer 内的一个 range 字节。底层ArrayBuffer 实际上表示要操作的连续二进制数据块,但我们需要一个视图来访问和操作该二进制数据的窗口。

多个不同的TypedArray 对象可以查看同一ArrayBuffer 中的单独(甚至重叠)范围。当您有两个共享相同ArrayBufferTypedArray 对象时,set 操作非常快。这是因为机器正在使用一个连续的内存块。

这是一个例子。我们将创建一个 32 字节的 ArrayBuffer,一个长度为 16 的 Uint8Array 表示缓冲区的前 16 个字节,另一个长度为 16 的 Uint8Array 表示最后 16 个字节:

var buffer = new ArrayBuffer(32);
var array1 = new Uint8Array(buffer,  0, 16);
var array2 = new Uint8Array(buffer, 16, 16);

现在我们可以在缓冲区的前半部分初始化一些值:

for (var i = 0; i < 16; i++) array1[i] = i;
console.log(array1); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
console.log(array2); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0,  0,  0,  0,  0,  0]

然后非常有效地将这 8 个字节复制到缓冲区的后半部分:

array2.set(array1);
console.log(array1); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
console.log(array2); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

我们可以通过用另一个视图查看缓冲区来确认两个数组实际上共享同一个缓冲区。例如,我们可以使用长度为 8 的 Uint32Array,它跨越整个 32 字节的缓冲区:

var array3 = new Uint32Array(buffer)
console.log(array3); // [50462976, 117835012, 185207048, 252579084, 
                     //  50462976, 117835012, 185207048, 252579084]

我修改了一个 JSPerf 测试,我发现它展示了在同一缓冲区上复制的巨大性能提升:

http://jsperf.com/typedarray-set-vs-loop/3

我们在 Chrome 和 Firefox 上获得了一个数量级的性能提升,它甚至比采用双倍长度的普通数组并将前半部分复制到后半部分要快得多。但是我们必须在这里考虑周期/内存权衡。只要我们引用了ArrayBuffer任何单个 视图,缓冲区的其余数据就不能被垃圾回收。为 ES7 Harmony 提出了一个 ArrayBuffer.transfer 函数,它可以解决这个问题,让我们能够显式释放内存而无需等待垃圾收集器,以及动态增长 ArrayBuffers 而无需复制的能力。

【讨论】:

  • 这是否仅适用于两个视图来自同一个 ArrayBuffer 的情况?我可以期望使用不同数组缓冲区的 2 个不同视图但长度和类型相同的相同性能提升吗?如果不是,为什么这个操作这么慢? (jsperf 在 atm 下)
【解决方案2】:

好吧,set 并不完全具有这样的简单语义,在 V8 中,在执行 some figuring out of what should be done 之后,它基本上会到达 exactly the same loop,其他方法首先直接执行。

请注意,如果您正确地玩牌(所有测试都这样做),Javascript 会被编译成高度优化的机器代码,因此不应该仅仅因为它们是“原生”而“崇拜”某些方法。

【讨论】:

  • 哇,感谢您在 src 中为我指出这一点。现在更有意义了。 :-)。所以基本上,它只是增加了典型 javascript 循环的开销,因此速度变慢了。
【解决方案3】:

我也一直在探索 set() 的执行方式,我不得不说,对于较小的块(例如原始海报使用的 16 个索引), set() 仍然比可比较的展开循环慢 5 倍左右,即使在连续的内存块上操作。

我已经改编了原始的 jsperf 测试 here。我认为公平地说,对于像这样的小块传输,set() 根本无法与展开的索引分配性能竞争。对于较大的块传输(如 sbking 的测试中所见),set() 确实表现更好,但它实际上与 100 万个数组索引操作竞争,因此无法用一条指令克服这一点似乎很疯狂。

我的测试中的连续缓冲区 set() 的性能确实比单独的缓冲区 set() 稍好,但同样,在这种传输大小下,性能优势是微不足道的

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-07-23
    • 1970-01-01
    • 1970-01-01
    • 2018-11-18
    • 2016-08-03
    • 1970-01-01
    • 1970-01-01
    • 2013-10-20
    相关资源
    最近更新 更多