追加ArrayBuffers

36

什么是将ArrayBuffers拼接/合并的首选方法?

我正在接收和解析各种数据结构的网络数据包。传入的消息被读入ArrayBuffers。如果部分数据包到达,我需要将其存储并等待下一条消息再次尝试解析它。

目前,我正在做类似于以下方式的事情:

function appendBuffer( buffer1, buffer2 ) {
  var tmp = new Uint8Array( buffer1.byteLength + buffer2.byteLength );
  tmp.set( new Uint8Array( buffer1 ), 0 );
  tmp.set( new Uint8Array( buffer2 ), buffer1.byteLength );
  return tmp.buffer;
}

很明显,由于ArrayBuffer是固定长度的,因此无法避免创建新缓冲区,但初始化typed arrays是否必要?我只是想将缓冲区视为缓冲区来处理;类型和结构并不重要。


2
可能是Gecko 2中的Typed Arrays:Float32Array连接和扩展的重复问题。 - Esailija
@Esailija,上述问题的解决方案提供了我的当前方法,即将类型化数组合并到新缓冲区中。当您想要处理类型化数组时,这是可以接受的。我想完全避免它们。我的问题是是否可能实现这一点。 - user1421750
2
你只能使用 ArrayBuffer.slice 方法,这样做的效果有限。然后你可以使用 BlobBuilder.append 方法,但这比你现在做的要复杂得多。你当前的方法有什么实际问题吗? - Esailija
2
@Esailija 我真正关心的是性能,虽然我还没有达到测试的那个点。这似乎只是一个拐弯抹角的方法。我还在适应JS!无论如何,还是谢谢。 - user1421750
每个ArrayBuffer都有固定的大小,因此复制数据是唯一的方法。您想使用本地代码进行复制,而不是Javascript循环,因此Uint8Array.set是您最好的选择 - 它可以检测其参数是否为另一个Uint8Array并执行高速C风格的memcpy。因此,您的原始代码示例几乎是最快且最简单的 - ArrayBuffer类没有有用的方法,因为其设计者预计类型化数组将是主要接口。出于这个原因,我更喜欢将Uint8Array用于“原始数据”,而不是ArrayBuffer - William Swanson
4个回答

8

为什么不使用Blob?(我知道那个时候可能还没有可用的Blob)。

只需使用您的数据创建一个Blob,例如var blob = new Blob([array1,array2,string,...]),如果需要,可以使用FileReader将其转换回ArrayBuffer(请参见this)。

查看此内容:BlobBuilder和新Blob构造函数之间有什么区别? 以及这个:MDN Blob API

编辑:

我想比较这两种方法(Blob和问题中使用的方法)的效率,并创建了一个JSPerf:http://jsperf.com/appending-arraybuffers

似乎使用Blob会更慢(实际上,我猜测是使用FileReader读取Blob所花费的时间最长)。现在你知道了 ;) 当有两个以上的ArrayBuffer时(例如从其块中重建文件),可能会更有效率。

1
但那是异步的... - Bergi
实际上,尽管如此,我仍然觉得它更容易使用。 - Jb Drucker
1
@Bergi 如果你在Workers中使用它,可以使用FileReaderSync - Константин Ван
@JbDrucker 你知道 blob 的内存是如何释放的吗?Blob.close() 看起来目前还处于 beta 版本。 - Doua Beri
对于我来说,今天(64位,Windows 10,Intel Core i7),您的测试第4版显示,在Firefox 56中使用Blobs速度提高了2倍,在Chrome 61中使用appendBuffer速度提高了6倍。 - John Glassmyer

4
function concat (views: ArrayBufferView[]) {
    let length = 0
    for (const v of views)
        length += v.byteLength
        
    let buf = new Uint8Array(length)
    let offset = 0
    for (const v of views) {
        const uint8view = new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
        buf.set(uint8view, offset)
        offset += uint8view.byteLength
    }
    
    return buf
}

对我很有效! 参考资料,Uint8Array.set() 在此链接中有详细的说明。 - personal_cloud

2

看起来你已经得出结论,没有办法绕过创建新的数组缓冲区。但是,为了性能考虑,将缓冲区的内容附加到标准数组对象中,然后从中创建新的数组缓冲区或类型化数组可能会有益。

var data = [];

function receive_buffer(buffer) {
    var i, len = data.length;

    for(i = 0; i < buffer.length; i++)
        data[len + i] = buffer[i];

    if( buffer_stream_done()) 
        callback( new Uint8Array(data));
}

大多数JavaScript引擎已经为动态分配内存设置了一些空间。该方法将利用该空间而不是创建大量新的内存分配,这可能会在操作系统内核中导致性能问题。此外,您还可以减少一些函数调用。
第二种更复杂的选项是预先分配内存。如果您知道任何数据流的最大大小,则可以创建该大小的数组缓冲区,填充它(必要时部分填充),然后在完成后清空它。
最后,如果性能是您的主要目标,并且您知道最大数据包大小(而不是整个流),则从那个大小开始使用一些数组缓冲区。当您填满预先分配的内存时,在网络调用之间创建新的缓冲区 - 如果可能异步进行。

1
我很想看到这个进行一些基准测试。 - Lupus Ossorum
如果我能学会使用JSperf XD,我可能会这样做。尽管如今,我不建议最后一种选择——它可能会变得太大而导致浏览器标签崩溃,甚至可能在Node中引起溢出(猜测)。 - Duco

-3

这与使用Uint8Array相比有何优势? - BHSPitMonkey
@BHSPitMonkey 这并不是真的 - 提及它的唯一原因是它不假设特定的元素类型。 - Cyphus
8
如果仅仅是复制数据,把一切视作Uint8是没有问题的,这在这个情况下就是这样。如果需要灵活性,DataView 是一个很好的选择,但是对于这个目的来说,它的性能要差得多得多。参见:http://jsperf.com/uint8array-vs-dataview3 - BHSPitMonkey

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