TypedArray Set vs. Unrolled Loop (Javascript)



在尝试为自己构建WebGL 3D库(主要是学习目的)时,我遵循了从各种来源找到的文档,这些文档指出TypedArray函数set()(特别是Float32Array),应该"和"C中的memcpy一样快(显然是开玩笑的),根据html5rocks,实际上是最快的。在看似正确的外观上(javascript中没有循环设置,消失在一些超快速类型数组中纯C废话等)。

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

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

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

是我的代码吗?我的浏览器?(我正在使用 Firefox 24)还是我缺少其他优化理论?任何有助于理解与我的想法和理解相反的结果都将非常有帮助。

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

多个不同的TypedArray对象可以查看同一ArrayBuffer中的单独(甚至重叠)范围。当你有两个TypedArray对象共享同一个ArrayBuffer时,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]

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

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函数,它将通过赋予我们无需等待垃圾收集器即可显式释放内存的能力以及动态增长ArrayBuffer而无需复制的能力来解决这个问题。

好吧,set并没有像这样简单的语义,在 V8 中,在弄清楚应该做什么之后,它基本上会到达与其他方法直接执行的完全相同的循环。

请注意,如果你打得对,JavaScript 会被编译成高度优化的机器代码(所有的测试都这样做),所以不应该仅仅因为它们是"原生的"而"崇拜"某些方法。

我也一直在探索 set() 的性能,我不得不说,对于较小的块(例如原始海报使用的 16 个索引),set() 仍然比类似的展开循环慢 5 倍左右,即使在连续的内存块上运行时也是如此。

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

在我的测试中,连续缓冲区 set() 的性能确实略好于单独的缓冲区 set(),但同样,在这种传输大小下,性能优势微乎其微

最新更新