From 686deb7779a8ddd1d233f5906dc0a18303eeff41 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Tue, 25 Mar 2025 15:27:50 +0100 Subject: [PATCH 1/2] assert,util: improve array comparison Sparse arrays and arrays containing undefined are now compared faster using assert.deepStrictEqual() or util.isDeepStrictEqual(). --- .../assert/deepequal-simple-array-and-set.js | 26 +++++++++++---- lib/internal/assert/myers_diff.js | 1 + lib/internal/util/comparisons.js | 33 ++++++++----------- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/benchmark/assert/deepequal-simple-array-and-set.js b/benchmark/assert/deepequal-simple-array-and-set.js index 08bbc87a1c5b1c..dd9351b63d5799 100644 --- a/benchmark/assert/deepequal-simple-array-and-set.js +++ b/benchmark/assert/deepequal-simple-array-and-set.js @@ -11,6 +11,8 @@ const bench = common.createBenchmark(main, { method: [ 'deepEqual_Array', 'notDeepEqual_Array', + 'deepEqual_sparseArray', + 'notDeepEqual_sparseArray', 'deepEqual_Set', 'notDeepEqual_Set', ], @@ -25,18 +27,30 @@ function run(fn, n, actual, expected) { } function main({ n, len, method, strict }) { - const actual = []; - const expected = []; + let actual = Array.from({ length: len }, (_, i) => i); + // Contain one undefined value to trigger a specific code path + actual[0] = undefined; + let expected = actual.slice(0); - for (let i = 0; i < len; i++) { - actual.push(i); - expected.push(i); - } if (method.includes('not')) { expected[len - 1] += 1; } switch (method) { + case 'deepEqual_sparseArray': + case 'notDeepEqual_sparseArray': + actual = new Array(len); + for (let i = 0; i < len; i += 2) { + actual[i] = i; + } + expected = actual.slice(0); + if (method.includes('not')) { + expected[len - 2] += 1; + run(strict ? notDeepStrictEqual : notDeepEqual, n, actual, expected); + } else { + run(strict ? deepStrictEqual : deepEqual, n, actual, expected); + } + break; case 'deepEqual_Array': run(strict ? deepStrictEqual : deepEqual, n, actual, expected); break; diff --git a/lib/internal/assert/myers_diff.js b/lib/internal/assert/myers_diff.js index 995bb9fd48a113..6fcfc4e84456fe 100644 --- a/lib/internal/assert/myers_diff.js +++ b/lib/internal/assert/myers_diff.js @@ -29,6 +29,7 @@ function myersDiff(actual, expected, checkCommaDisparity = false) { const actualLength = actual.length; const expectedLength = expected.length; const max = actualLength + expectedLength; + // TODO(BridgeAR): Cap the input in case the values go beyond the limit of 2^31 - 1. const v = new Int32Array(2 * max + 1); const trace = []; diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 7eb9c72119eb92..a266a78f6d96ec 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -196,11 +196,9 @@ function innerDeepEqual(val1, val2, mode, memos) { } } else { if (val1 === null || typeof val1 !== 'object') { - if (val2 === null || typeof val2 !== 'object') { - // eslint-disable-next-line eqeqeq - return val1 == val2 || (NumberIsNaN(val1) && NumberIsNaN(val2)); - } - return false; + return (val2 === null || typeof val2 !== 'object') && + // eslint-disable-next-line eqeqeq + (val1 == val2 || (NumberIsNaN(val1) && NumberIsNaN(val2))); } if (val2 === null || typeof val2 !== 'object') { return false; @@ -384,9 +382,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { } } else if (keys2.length !== ObjectKeys(val1).length) { return false; - } - - if (mode === kStrict) { + } else if (mode === kStrict) { const symbolKeysA = getOwnSymbols(val1); if (symbolKeysA.length !== 0) { let count = 0; @@ -758,9 +754,9 @@ function sparseArrayEquiv(a, b, mode, memos, i) { if (keysA.length !== keysB.length) { return false; } - for (; i < keysA.length; i++) { - const key = keysA[i]; - if (!hasOwn(b, key) || !innerDeepEqual(a[key], b[key], mode, memos)) { + for (; i < keysB.length; i++) { + const key = keysB[i]; + if ((a[key] === undefined && !hasOwn(a, key)) || !innerDeepEqual(a[key], b[key], mode, memos)) { return false; } } @@ -782,17 +778,14 @@ function objEquiv(a, b, mode, keys2, memos, iterationType) { return partialArrayEquiv(a, b, mode, memos); } for (let i = 0; i < a.length; i++) { - if (!innerDeepEqual(a[i], b[i], mode, memos)) { - return false; - } - const isSparseA = a[i] === undefined && !hasOwn(a, i); - const isSparseB = b[i] === undefined && !hasOwn(b, i); - if (isSparseA !== isSparseB) { + if (b[i] === undefined) { + if (!hasOwn(b, i)) + return sparseArrayEquiv(a, b, mode, memos, i); + if (a[i] !== undefined || !hasOwn(a, i)) + return false; + } else if (a[i] === undefined || !innerDeepEqual(a[i], b[i], mode, memos)) { return false; } - if (isSparseA) { - return sparseArrayEquiv(a, b, mode, memos, i); - } } } else if (iterationType === kIsSet) { if (!setEquiv(a, b, mode, memos)) { From f46895a01ed5842c222eec9d8d9e0456c8f816d7 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Wed, 26 Mar 2025 01:00:49 +0100 Subject: [PATCH 2/2] assert,util: improve unequal number comparison performance This improves the performance to compare unequal numbers while doing a deep equal comparison. Comparing for NaN is faster by checking `variable !== variable` than by using `Number.isNaN()`. --- lib/internal/util/comparisons.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index a266a78f6d96ec..8e2988e439d17b 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -8,7 +8,6 @@ const { BooleanPrototypeValueOf, DatePrototypeGetTime, Error, - NumberIsNaN, NumberPrototypeValueOf, ObjectGetOwnPropertySymbols: getOwnSymbols, ObjectGetPrototypeOf, @@ -185,7 +184,9 @@ function innerDeepEqual(val1, val2, mode, memos) { // Check more closely if val1 and val2 are equal. if (mode !== kLoose) { if (typeof val1 === 'number') { - return NumberIsNaN(val1) && NumberIsNaN(val2); + // Check for NaN + // eslint-disable-next-line no-self-compare + return val1 !== val1 && val2 !== val2; } if (typeof val2 !== 'object' || typeof val1 !== 'object' || @@ -197,8 +198,9 @@ function innerDeepEqual(val1, val2, mode, memos) { } else { if (val1 === null || typeof val1 !== 'object') { return (val2 === null || typeof val2 !== 'object') && - // eslint-disable-next-line eqeqeq - (val1 == val2 || (NumberIsNaN(val1) && NumberIsNaN(val2))); + // Check for NaN + // eslint-disable-next-line eqeqeq, no-self-compare + (val1 == val2 || (val1 !== val1 && val2 !== val2)); } if (val2 === null || typeof val2 !== 'object') { return false; @@ -498,7 +500,9 @@ function findLooseMatchingPrimitives(prim) { // a regular number and not NaN. // Fall through case 'number': - if (NumberIsNaN(prim)) { + // Check for NaN + // eslint-disable-next-line no-self-compare + if (prim !== prim) { return false; } }