Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions bytes/equals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
}
20 changes: 19 additions & 1 deletion bytes/equals_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
}
}
});
Loading