perf(bytes): improve performance of equals() (#4635)

* improve performance of bytes equals

* fmt

* comment

* comment

---------

Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
T6 2024-04-23 20:59:32 -04:00 committed by GitHub
parent d78321d902
commit b2115344fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 49 additions and 8 deletions

View File

@ -27,18 +27,38 @@ function equalsNaive(a: Uint8Array, b: Uint8Array): boolean {
*/
function equals32Bit(a: Uint8Array, b: Uint8Array): boolean {
const len = a.length;
const compressible = Math.floor(len / 4);
const compressedA = new Uint32Array(a, 0, compressible);
const compressedB = new Uint32Array(b, 0, compressible);
for (let i = compressible * 4; i < len; i++) {
const compactOffset = 3 - ((a.byteOffset + 3) % 4);
const compactLen = Math.floor((len - compactOffset) / 4);
const compactA = new Uint32Array(
a.buffer,
a.byteOffset + compactOffset,
compactLen,
);
const compactB = new Uint32Array(
b.buffer,
b.byteOffset + compactOffset,
compactLen,
);
for (let i = 0; i < compactOffset; i++) {
if (a[i] !== b[i]) return false;
}
for (let i = 0; i < compressedA.length; i++) {
if (compressedA[i] !== compressedB[i]) return false;
for (let i = 0; i < compactA.length; i++) {
if (compactA[i] !== compactB[i]) return false;
}
for (let i = compactOffset + compactLen * 4; i < len; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
/**
* Byte length threshold for when to use 32-bit comparisons, based on
* benchmarks.
*
* @see {@link https://github.com/denoland/deno_std/pull/4635}
*/
const THRESHOLD_32_BIT = 160;
/**
* Check whether byte slices are equal to each other.
*
@ -62,5 +82,8 @@ export function equals(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
return false;
}
return a.length < 1000 ? equalsNaive(a, b) : equals32Bit(a, b);
return a.length >= THRESHOLD_32_BIT &&
(a.byteOffset % 4) === (b.byteOffset % 4)
? equals32Bit(a, b)
: equalsNaive(a, b);
}

View File

@ -11,9 +11,11 @@ Deno.test("equals()", () => {
assert(!v3);
});
const THRESHOLD_32_BIT = 160;
Deno.test("equals() handles randomized testing", () => {
// run tests before and after cutoff
for (let len = 995; len <= 1005; len++) {
for (let len = THRESHOLD_32_BIT - 10; len <= THRESHOLD_32_BIT + 10; len++) {
const arr1 = crypto.getRandomValues(new Uint8Array(len));
const arr2 = crypto.getRandomValues(new Uint8Array(len));
const arr3 = arr1.slice(0);
@ -47,4 +49,20 @@ Deno.test("equals() works with .subarray()", () => {
c[999] = 123;
assertNotEquals(c, d); // ok
assert(!equals(c, d));
// Test every length/offset combination (modulo 4) to ensure that every byte is checked.
for (let offsetA = 0; offsetA < 4; offsetA++) {
for (let offsetB = 0; offsetB < 4; offsetB++) {
for (let len = THRESHOLD_32_BIT; len < THRESHOLD_32_BIT + 4; len++) {
const x = new Uint8Array(new ArrayBuffer(len + offsetA), offsetA);
const y = new Uint8Array(new ArrayBuffer(len + offsetB), offsetB);
for (let i = 0; i < len; i++) {
assert(equals(x, y));
x[i] = 1;
assert(!equals(x, y));
y[i] = 1;
}
}
}
}
});