如何在ArrayBuffer、DataView和TypedArray中进行相等性测试

24
有没有一种方法可以测试两个JavaScript ArrayBuffers是否相等? 我想为消息组合方法编写测试。 我找到的唯一方法是将ArrayBuffer转换为字符串,然后进行比较。 我错过了什么吗?
以下代码会返回false,即使我认为它应该是true:
(function() {
    'use strict';

    /* Fill buffer with data of Verse header and user_auth
     * command */
    var buf_pos = 0;
    var name_len = 6
    var message_len = 4 + 1 + 1 + 1 + name_len + 1;

    var buf = new ArrayBuffer(message_len);
    var view = new DataView(buf);
    /* Verse header starts with version */
    view.setUint8(buf_pos, 1 << 4); /* First 4 bits are reserved for version of protocol */
    buf_pos += 2;
    /* The lenght of the message */
    view.setUint16(buf_pos, message_len);
    buf_pos += 2;

    buf_pos = 0;
    var buf2 = new ArrayBuffer(message_len);
    var view2 = new DataView(buf);
    /* Verse header starts with version */
    view2.setUint8(buf_pos, 1 << 4); /* First 4 bits are reserved for version of protocol */
    buf_pos += 2;
    /* The lenght of the message */
    view2.setUint16(buf_pos, message_len);
    buf_pos += 2;


    if(buf == buf2){
        console.log('true');
    }
    else{
        console.log('false');
    }


}());
如果我试图比较view和view2,结果还是假的。
7个回答

22
在JavaScript中,你不能直接使用=====比较两个对象。这些运算符只会检查引用的相等性(即表达式是否引用同一个对象)。
但是,你可以使用DataViewArrayView对象检索ArrayBuffer对象的特定部分的值并对它们进行检查。
如果你想要检查头文件:
if (  view1.getUint8 (0) == view2.getUint8 (0)
   && view1.getUint16(2) == view2.getUint16(2)) ...

或者,如果您想检查缓冲区的全局性:

function equal (buf1, buf2)
{
    if (buf1.byteLength != buf2.byteLength) return false;
    var dv1 = new Int8Array(buf1);
    var dv2 = new Int8Array(buf2);
    for (var i = 0 ; i != buf1.byteLength ; i++)
    {
        if (dv1[i] != dv2[i]) return false;
    }
    return true;
}
如果你想基于ArrayBuffer实现一个复杂的数据结构,我建议创建自己的类,否则每次你想将火柴棒移进或移出结构时都需要使用笨重的原始DataView/ArrayView实例。

谢谢。这正是我需要做的——实现一组用于处理WebSocket消息的类。我希望能够对方法和状态机进行测试覆盖。 - JirkaV
猜测一下...如果我们将两个数组都转换为字符串(比如base 64),然后再进行比较,这样行得通吗? - Amarsh
可能可以,但我怀疑那样不会更有效率。 - kuroi neko
考虑使用 DataView 来提高性能。请参见下面的答案。 - anthumchris
@AnthumChris 为什么?Int8Array也只是底层缓冲区的“视图”,它不会复制它。 - Joakim L. Christiansen
我更多的是考虑到源代码的可读性,如果你在其中满目皆是getUIntxx函数调用,那么源代码将变得难以阅读。此外,如果你把整个处理过程放在一个整洁的模块中,则在结构和原始访问之间保持一致性可能会更容易。老实说,我不知道JS在优化方面取得了多大进展,也不知道哪种方法比另一种更快。 - kuroi neko

9

在一般的JavaScript中,你现在必须通过使用TypedArray将每个ArrayBuffer对象进行包装,并手动迭代每个元素并进行逐个比较来进行比较。

如果底层缓冲区是2或4字节的内存对齐,则可以通过使用Uint16或Uint32类型数组进行比较来进行显着优化。

/**
 * compare two binary arrays for equality
 * @param {(ArrayBuffer|ArrayBufferView)} a
 * @param {(ArrayBuffer|ArrayBufferView)} b 
 */
function equal(a, b) {
  if (a instanceof ArrayBuffer) a = new Uint8Array(a, 0);
  if (b instanceof ArrayBuffer) b = new Uint8Array(b, 0);
  if (a.byteLength != b.byteLength) return false;
  if (aligned32(a) && aligned32(b))
    return equal32(a, b);
  if (aligned16(a) && aligned16(b))
    return equal16(a, b);
  return equal8(a, b);
}

function equal8(a, b) {
  const ua = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
  const ub = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
  return compare(ua, ub);
}
function equal16(a, b) {
  const ua = new Uint16Array(a.buffer, a.byteOffset, a.byteLength / 2);
  const ub = new Uint16Array(b.buffer, b.byteOffset, b.byteLength / 2);
  return compare(ua, ub);
}
function equal32(a, b) {
  const ua = new Uint32Array(a.buffer, a.byteOffset, a.byteLength / 4);
  const ub = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / 4);
  return compare(ua, ub);
}

function compare(a, b) {
  for (let i = a.length; -1 < i; i -= 1) {
    if ((a[i] !== b[i])) return false;
  }
  return true;
}

function aligned16(a) {
  return (a.byteOffset % 2 === 0) && (a.byteLength % 2 === 0);
}

function aligned32(a) {
  return (a.byteOffset % 4 === 0) && (a.byteLength % 4 === 0);
}

并通过以下方式调用:

equal(buf1, buf2)

这里是针对1、2、4字节对齐内存的性能测试

输入图像描述 输入图像描述

替代方案:

您也可以通过WASM获得更好的性能,但将数据传输到堆可能会抵消比较的好处。

在Node.JS中,您可以使用Buffer来获得更好的性能,因为它具有本地代码:Buffer.from(buf1, 0).equals(Buffer.from(buf2, 0))


这段代码有两个bug:
  1. 如果传递的类型化数组不是 Int8ArrayUint8ArrayUint8ClampedArray 的一个实例,equal 将无法正确工作,因为 .length 将返回元素单位长度而不是字节长度。您应该使用 .byteLength
- emk
1
很不幸,equal64 无论多么快都是没用的。你不能使用浮点数来比较任意的缓冲区。它既有误报也有漏报:equal64(new Uint8Array([0,0,0,0,0,0,0,0]), new Uint8Array([0,0,0,0,0,0,0,0x80])); // +0 和 -0 → true equal64(new Uint8Array([0,0,0,0,0,0,0xF8,0xFF]), new Uint8Array([0,0,0,0,0,0,0xF8,0xFF])); // NaN → false - emk
@emk,我确实对float64点有所疑惑,感谢您的证明。非常感谢,我会相应地更新和修复。 - Meirion Hughes
1
请注意,BigUint64Array 现已发布。 - gsnedders
2
您还可以通过比较对齐的前缀(例如,给定131个字节,将前131个字节作为更长的单词进行比较,然后逐个比较最后三个字节)来进一步优化测试。 - gsnedders
显示剩余2条评论

5

要比较两个TypedArrays之间的相等性,请考虑使用every方法,一旦发现不一致就会退出:

const a = Uint8Array.from([0,1,2,3]);
const b = Uint8Array.from([0,1,2,3]);
const c = Uint8Array.from([0,1,2,3,4]);
const areEqual = (first, second) =>
    first.length === second.length && first.every((value, index) => value === second[index]);

console.log(areEqual(a, b));
console.log(areEqual(a, c));

这比其它替代方案(如使用toString() 进行比较)更为经济高效,在找到不同之后,它可以避免继续迭代数组的剩余部分。


3
不错的解决方案,但需要加上长度检查。如果“second”比“first”长且前面的字节相等,则无法正常工作。 - Perry Mitchell

4
今天的V8中,DataView现在可以用于“性能关键的实际应用程序”—— https://v8.dev/blog/dataview 下面的函数基于您已经实例化的对象测试相等性。如果您已经有了TypedArray对象,则可以直接比较它们,而无需为它们创建额外的DataView对象(欢迎有人为两种选项测量性能)。
// compare ArrayBuffers
function arrayBuffersAreEqual(a, b) {
  return dataViewsAreEqual(new DataView(a), new DataView(b));
}

// compare DataViews
function dataViewsAreEqual(a, b) {
  if (a.byteLength !== b.byteLength) return false;
  for (let i=0; i < a.byteLength; i++) {
    if (a.getUint8(i) !== b.getUint8(i)) return false;
  }
  return true;
}

// compare TypedArrays
function typedArraysAreEqual(a, b) {
  if (a.byteLength !== b.byteLength) return false;
  return a.every((val, i) => val === b[i]);
}

1
我编写了这些函数来比较最常见的数据类型。它适用于ArrayBuffer、TypedArray、DataView、Node.js缓冲区和任何包含字节数据(0-255)的普通数组。
// It will not copy any underlying buffers, instead it will create a view into them.
function dataToUint8Array(data) {
  let uint8array
  if (data instanceof ArrayBuffer || Array.isArray(data)) {
    uint8array = new Uint8Array(data)
  } else if (data instanceof Buffer) { // Node.js Buffer
    uint8array = new Uint8Array(data.buffer, data.byteOffset, data.length)
  } else if (ArrayBuffer.isView(data)) { // DataView, TypedArray or Node.js Buffer
    uint8array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
  } else {
    throw Error('Data is not an ArrayBuffer, TypedArray, DataView or a Node.js Buffer.')
  }
  return uint8array
}

function compareData(a, b) {
  a = dataToUint8Array(a); b = dataToUint8Array(b)
  if (a.byteLength != b.byteLength) return false
  return a.every((val, i) => val == b[i])
}

0
IndexedDB API 提供了一种内置方法,可以按字节比较两个 ArrayBuffer 或 ArrayBuffer 视图。这是因为 IndexedDB 有一个"键顺序"的概念,在 IndexedDB 中,一些 JavaScript 值的子集可以用作"键",并且具有定义好的排序顺序。重要的是,其中一种键的类型是二进制键,在规范中被定义为"ArrayBuffer 对象(或类似 Uint8Array 的缓冲区视图)"
对此有一些注意事项:
  • 通常只在浏览器环境下可用。在NodeJS中,您可以使用基于缓冲区的选项。我对其他情况不太确定。
  • 仅在提供的IndexedDB API为v2.0或更高版本时起作用,因为二进制键不是v1.0的功能。根据caniuse.com的数据,v2.0 可用 在基本上 任何支持IndexedDB的浏览器 中,这是绝大多数情况(除了IE,显然甚至从未完全支持v1.0)。
  • 由于比较是按字节进行的,您可以互换地比较 ArrayBufferUint8ArrayDataView。然而,其他类型的Typed Array 则要复杂一些:
    • 通常,如果比较的两侧都是同一类型的Typed Array(即两个 Int32Array、两个 Uint16Array,等等),那么您至少可以使用此方法进行相等性测试。但即使如此,也有一个地方可能会给出令人惊讶的结果:在浮点数组中,“负”零与“正”零具有不同的位模式,因此 new Float64Array([0])new Float64Array([-0]) 不会被视为相等。这种怪异情况不适用于整数数组。
    • 任何其他类型的比较可能不会给出您期望的结果。例如,new Int8Array([-1]) 会比 new Int8Array([0]) 大。在某些机器上,new Int32Array([1024]) 可能小于 new Int32Array([1]),但在其他机器上可能大(!)这是由于Typed Array使用本机字节序。浮点数组将以似乎随机的方式将不同的值与大于或小于进行比较。
    • 基本上,如果您想要对 Typed Array 数组进行排序,这种机制只有在它们全部都是 Uint8Array 时才有帮助。
  • 这有点奇怪
说了这么多,使用这种方法进行二进制相等性测试非常简单:
function areBytewiseEqual(a, b) {
  return indexedDB.cmp(a, b) === 0;
}

-1
您可以将数组转换为字符串并进行比较。例如:
let a = new Uint8Array([1, 2, 3, 4]);
let b = new Uint8Array([1, 2, 3, 4]);
if (a.toString() == b.toString()) {
    console.log("Yes");
} else {
    console.log("No");
}

7
如果其中一个缓冲区两边都填充了0,这种方法可能不起作用。如果你使用toString方法将填充0的缓冲区转换为字符串,返回的字符串长度将不反映实际字符串的长度,因此字符串比较将无法正常工作。例如, Buffer.from([80]).toString() 的结果是 "P",Buffer.from([80, 0]).toString() 的结果也是 "P",但第二个字符串的长度为2。尽管明显只有一个字母。修剪字符串似乎也行不通。 - AlexMorley-Finch
如果您使用的是节点缓冲区,则会出现这种情况。然而,此答案使用视图 - uint8array,调用toString方法返回连接在一起的值,中间用“,”分隔。而在您的示例中,使用了某些字符串编码方法。即使如此,长度差异也是可取的,因为它可以防止碰撞。 - Franartur Čech

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