2

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

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

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

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

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

4

3 回答 3

4

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

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

这是一个例子。我们将创建一个ArrayBuffer32 字节的长度,一个长度为 16Uint8Array表示缓冲区的前 16 个字节,另一个长度为 16Uint8Array表示最后 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]

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

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

于 2015-03-22T20:09:45.683 回答
1

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

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

于 2013-10-24T20:07:23.777 回答
0

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

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

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

于 2015-05-23T22:33:59.370 回答