Question

Is there a way how to test if two JavaScript ArrayBuffers are equal? I would like to write test for message composing method. The only way I found is to convert the ArrayBuffer to string and then compare. Did I miss something?

Following code is giving false, even if I think that it should be 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');
    }


}());

If I try to compare view and view2 it's false again.

Was it helpful?

Solution

You cannot compare two objects directly in JavaScript using == or ===.
These operators will only check the equality of references (i.e. if expressions reference the same object).

You can, however, use DataView or ArrayView objects to retrieve values of specific parts of ArrayBuffer objects and check them.

If you want to check headers:

if (  view1.getUint8 (0) == view2.getUint8 (0)
   && view1.getUint16(2) == view2.getUint16(2)) ...

Or if you want to check the globality of your buffers:

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;
}

If you want to implement a complex data structure based on ArrayBuffer, I suggest creating your own class, or else you will have to resort to cumbersome raw DataView / ArrayView instances each time you will want to move a matchstick in and out of the structure.

OTHER TIPS

In general javascript, you currently have to compare two ArrayBuffer objects by wrapping each with a TypedArray, then manually iterating over each element and doing element-wise equality.

If the underlying buffer is 2 or 4-byte memory-aligned then you can make a significant optimization by employing Uint16 or Uint32 typed-arrays for the comparison.

/**
 * 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);
}

and called via:

equal(buf1, buf2)

here are the performance tests for 1-, 2-, 4-byte aligned memory.

enter image description here enter image description here

Alternatives:

You may also get more performance with WASM, but its possible the cost of transferring the data to the heap may negate the comparison benefit.

Within Node.JS you may get more performance with Buffer as it will have native code: Buffer.from(buf1, 0).equals(Buffer.from(buf2, 0))

To test for equality between two TypedArrays, consider using the every method, which exits as soon as an inconsistency is found:

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));

This is less expensive than alternatives (like toString() comparisons) which iterate over the remaining array even after a difference is found.

In today's V8, DataView should now be "usable for performance-critical real-world applications" — https://v8.dev/blog/dataview

The functions below test equality based on the objects you already have instantiated. If you already have TypedArray objects, you could compare them directly without creating additional DataView objects for them (someone is welcome to measure performance for both options).

// 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]);
}

I wrote these functions to compare the most normal data types. It works with ArrayBuffer, TypedArray, DataView, Node.js Buffer and any normal Array with byte data (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])
}

You can always convert the arrays into strings and compare them. E.g.

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");
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top