From 64f56e415693106a7ca6765794b5c3cd12f963a7 Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Sat, 8 Mar 2025 18:02:42 +0100 Subject: [PATCH] assert,util: improve performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves the performance for array comparison by making the sparse array detection simpler. On top of that it adds a fast path for sets and maps that only contain objects as key. PR-URL: https://github.com/nodejs/node/pull/57370 Reviewed-By: Rafael Gonzaga Reviewed-By: Vinícius Lourenço Claro Cardoso --- lib/internal/util/comparisons.js | 129 ++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 46 deletions(-) diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 3c69b2763f..67307d722b 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -559,6 +559,35 @@ function partialObjectSetEquiv(a, b, mode, set, memo) { } } +function setObjectEquiv(a, b, mode, set, memo) { + if (mode === kPartial) { + return partialObjectSetEquiv(a, b, mode, set, memo); + } + // Fast path for objects only + if (mode === kStrict && set.size === a.size) { + for (const val of a) { + if (!setHasEqualElement(set, val, mode, memo)) { + return false; + } + } + return true; + } + + for (const val of a) { + // Primitive values have already been handled above. + if (typeof val === 'object') { + if (!b.has(val) && !setHasEqualElement(set, val, mode, memo)) { + return false; + } + } else if (mode === kLoose && + !b.has(val) && + !setHasEqualElement(set, val, mode, memo)) { + return false; + } + } + return set.size === 0; +} + function setEquiv(a, b, mode, memo) { // This is a lazily initiated Set of entries which have to be compared // pairwise. @@ -584,22 +613,7 @@ function setEquiv(a, b, mode, memo) { } if (set !== null) { - if (mode === kPartial) { - return partialObjectSetEquiv(a, b, mode, set, memo); - } - for (const val of a) { - // Primitive values have already been handled above. - if (typeof val === 'object' && val !== null) { - if (!b.has(val) && !setHasEqualElement(set, val, mode, memo)) { - return false; - } - } else if (mode === kLoose && - !b.has(val) && - !setHasEqualElement(set, val, mode, memo)) { - return false; - } - } - return set.size === 0; + return setObjectEquiv(a, b, mode, set, memo); } return true; @@ -640,6 +654,35 @@ function partialObjectMapEquiv(a, b, mode, set, memo) { } } +function mapObjectEquivalence(a, b, mode, set, memo) { + if (mode === kPartial) { + return partialObjectMapEquiv(a, b, mode, set, memo); + } + // Fast path for objects only + if (mode === kStrict && set.size === a.size) { + for (const { 0: key1, 1: item1 } of a) { + if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) { + return false; + } + } + return true; + } + for (const { 0: key1, 1: item1 } of a) { + if (typeof key1 === 'object' && key1 !== null) { + if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) + return false; + } else if (set.size === 0) { + return true; + } else if (mode === kLoose && + (!b.has(key1) || + !innerDeepEqual(item1, b.get(key1), mode, memo)) && + !mapHasEqualEntry(set, b, key1, item1, mode, memo)) { + return false; + } + } + return set.size === 0; +} + function mapEquiv(a, b, mode, memo) { let set = null; @@ -675,21 +718,7 @@ function mapEquiv(a, b, mode, memo) { } if (set !== null) { - if (mode === kPartial) { - return partialObjectMapEquiv(a, b, mode, set, memo); - } - for (const { 0: key1, 1: item1 } of a) { - if (typeof key1 === 'object' && key1 !== null) { - if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) - return false; - } else if (mode === kLoose && - (!b.has(key1) || - !innerDeepEqual(item1, b.get(key1), mode, memo)) && - !mapHasEqualEntry(set, b, key1, item1, mode, memo)) { - return false; - } - } - return set.size === 0; + return mapObjectEquivalence(a, b, mode, set, memo); } return true; @@ -737,6 +766,24 @@ function partialArrayEquiv(a, b, mode, memos) { return true; } +function sparseArrayEquiv(a, b, mode, memos, i) { + // TODO(BridgeAR): Use internal method to only get index properties. The + // same applies to the partial implementation. + const keysA = ObjectKeys(a); + const keysB = ObjectKeys(b); + if (keysA.length !== keysB.length) { + return false; + } + for (; i < keysA.length; i++) { + const key = keysA[i]; + if (!ObjectPrototypeHasOwnProperty(b, key) || + !innerDeepEqual(a[key], b[key], mode, memos)) { + return false; + } + } + return true; +} + function objEquiv(a, b, mode, keys2, memos, iterationType) { // The pair must have equivalent values for every corresponding key. if (keys2.length > 0) { @@ -755,23 +802,13 @@ function objEquiv(a, b, mode, keys2, memos, iterationType) { if (!innerDeepEqual(a[i], b[i], mode, memos)) { return false; } - const isOwnProperty = ObjectPrototypeHasOwnProperty(a, i); - if (isOwnProperty !== ObjectPrototypeHasOwnProperty(b, i)) { + const isSparseA = a[i] === undefined && !ObjectPrototypeHasOwnProperty(a, i); + const isSparseB = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i); + if (isSparseA !== isSparseB) { return false; } - if (!isOwnProperty) { - // Array is sparse. - // TODO(BridgeAR): Use internal method to only get index properties. The - // same applies to the partial implementation. - const keysA = ObjectKeys(a); - for (; i < keysA.length; i++) { - const key = keysA[i]; - if (!ObjectPrototypeHasOwnProperty(b, key) || - !innerDeepEqual(a[key], b[key], mode, memos)) { - return false; - } - } - return keysA.length === ObjectKeys(b).length; + if (isSparseA) { + return sparseArrayEquiv(a, b, mode, memos, i); } } } else if (iterationType === kIsSet) {