assert,util: improve comparison performance

This makes sure that the toStringTag symbol is used, if available
instead of calculating the toString() value each time, if not
needed (the type checks make a brand check, so there is no need to
check the toStringTag, if non is defined on the object).

PR-URL: https://github.com/nodejs/node/pull/61176
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
This commit is contained in:
Ruben Bridgewater
2025-12-29 23:50:44 +07:00
committed by GitHub
parent 879b95efea
commit 5c8ce910bd

View File

@@ -41,6 +41,7 @@ const {
StringPrototypeValueOf,
Symbol,
SymbolPrototypeValueOf,
SymbolToStringTag,
TypedArrayPrototypeGetByteLength: getByteLength,
TypedArrayPrototypeGetSymbolToStringTag,
Uint16Array,
@@ -264,6 +265,17 @@ function innerDeepEqual(val1, val2, mode, memos) {
return objectComparisonStart(val1, val2, mode, memos);
}
function hasUnequalTag(val1, val2) {
return val1[SymbolToStringTag] !== val2[SymbolToStringTag];
}
function slowHasUnequalTag(val1Tag, val1, val2) {
if (val1[SymbolToStringTag] !== undefined && val2[SymbolToStringTag] !== undefined) {
return val1[SymbolToStringTag] !== val2[SymbolToStringTag];
}
return val1Tag !== ObjectPrototypeToString(val2);
}
function objectComparisonStart(val1, val2, mode, memos) {
if (mode === kStrict) {
if (wellKnownConstructors.has(val1.constructor) ||
@@ -276,16 +288,10 @@ function objectComparisonStart(val1, val2, mode, memos) {
}
}
const val1Tag = ObjectPrototypeToString(val1);
const val2Tag = ObjectPrototypeToString(val2);
if (val1Tag !== val2Tag) {
return false;
}
if (ArrayIsArray(val1)) {
if (!ArrayIsArray(val2) ||
(val1.length !== val2.length && (mode !== kPartial || val1.length < val2.length))) {
(val1.length !== val2.length && (mode !== kPartial || val1.length < val2.length)) ||
hasUnequalTag(val1, val2)) {
return false;
}
@@ -296,22 +302,29 @@ function objectComparisonStart(val1, val2, mode, memos) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsArray, keys2);
} else if (val1Tag === '[object Object]') {
}
let val1Tag;
if (val1[SymbolToStringTag] === undefined &&
(val1Tag = ObjectPrototypeToString(val1)) === '[object Object]') {
if (slowHasUnequalTag(val1Tag, val1, val2)) {
return false;
}
return keyCheck(val1, val2, mode, memos, kNoIterator);
} else if (isDate(val1)) {
if (!isDate(val2)) {
} else if (isSet(val1)) {
if (!isSet(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size)) ||
hasUnequalTag(val1, val2)) {
return false;
}
const time1 = DatePrototypeGetTime(val1);
const time2 = DatePrototypeGetTime(val2);
// eslint-disable-next-line no-self-compare
if (time1 !== time2 && (time1 === time1 || time2 === time2)) {
return false;
}
} else if (isRegExp(val1)) {
if (!isRegExp(val2) || !areSimilarRegExps(val1, val2)) {
return keyCheck(val1, val2, mode, memos, kIsSet);
} else if (isMap(val1)) {
if (!isMap(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size)) ||
hasUnequalTag(val1, val2)) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsMap);
} else if (isArrayBufferView(val1)) {
if (TypedArrayPrototypeGetSymbolToStringTag(val1) !==
TypedArrayPrototypeGetSymbolToStringTag(val2)) {
@@ -339,20 +352,22 @@ function objectComparisonStart(val1, val2, mode, memos) {
return false;
}
return keyCheck(val1, val2, mode, memos, kNoIterator, keys2);
} else if (isSet(val1)) {
if (!isSet(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
} else if (isDate(val1)) {
if (!isDate(val2) || hasUnequalTag(val1, val2)) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsSet);
} else if (isMap(val1)) {
if (!isMap(val2) ||
(val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) {
const time1 = DatePrototypeGetTime(val1);
const time2 = DatePrototypeGetTime(val2);
// eslint-disable-next-line no-self-compare
if (time1 !== time2 && (time1 === time1 || time2 === time2)) {
return false;
}
} else if (isRegExp(val1)) {
if (!isRegExp(val2) || !areSimilarRegExps(val1, val2) || hasUnequalTag(val1, val2)) {
return false;
}
return keyCheck(val1, val2, mode, memos, kIsMap);
} else if (isAnyArrayBuffer(val1)) {
if (!isAnyArrayBuffer(val2)) {
if (!isAnyArrayBuffer(val2) || hasUnequalTag(val1, val2)) {
return false;
}
if (mode !== kPartial || val1.byteLength === val2.byteLength) {
@@ -362,6 +377,15 @@ function objectComparisonStart(val1, val2, mode, memos) {
} else if (!isPartialUint8Array(new Uint8Array(val1), new Uint8Array(val2))) {
return false;
}
} else if (slowHasUnequalTag(val1Tag ?? ObjectPrototypeToString(val1), val1, val2) ||
ArrayIsArray(val2) ||
isArrayBufferView(val2) ||
isSet(val2) ||
isMap(val2) ||
isDate(val2) ||
isRegExp(val2) ||
isAnyArrayBuffer(val2)) {
return false;
} else if (isError(val1)) {
// Do not compare the stack as it might differ even though the error itself
// is otherwise identical.
@@ -380,17 +404,6 @@ function objectComparisonStart(val1, val2, mode, memos) {
if (!isEqualBoxedPrimitive(val1, val2)) {
return false;
}
} else if (ArrayIsArray(val2) ||
isArrayBufferView(val2) ||
isSet(val2) ||
isMap(val2) ||
isDate(val2) ||
isRegExp(val2) ||
isAnyArrayBuffer(val2) ||
isBoxedPrimitive(val2) ||
isNativeError(val2) ||
val2 instanceof Error) {
return false;
} else if (isURL(val1)) {
if (!isURL(val2) || val1.href !== val2.href) {
return false;
@@ -412,7 +425,12 @@ function objectComparisonStart(val1, val2, mode, memos) {
) {
return false;
}
} else if (isWeakMap(val1) || isWeakSet(val1) || isPromise(val1)) {
} else if (isBoxedPrimitive(val2) ||
isNativeError(val2) ||
val2 instanceof Error ||
isWeakMap(val1) ||
isWeakSet(val1) ||
isPromise(val1)) {
return false;
}
@@ -879,10 +897,8 @@ function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
let aPos = startA;
const keysA = ObjectKeys(a);
const keysB = ObjectKeys(b);
const keysBLength = keysB.length;
const keysALength = keysA.length;
const lenA = keysALength - startA;
const lenB = keysBLength - startB;
const lenA = keysA.length - startA;
const lenB = keysB.length - startB;
if (lenA < lenB) {
return false;
}
@@ -890,7 +906,7 @@ function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) {
const keyB = keysB[startB + i];
while (!innerDeepEqual(a[keysA[aPos]], b[keyB], mode, memos)) {
aPos++;
if (aPos > keysALength - lenB + i) {
if (aPos > keysA.length - lenB + i) {
return false;
}
}
@@ -922,8 +938,6 @@ function partialArrayEquiv(a, b, mode, memos) {
}
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) {