Repository: nodejs/node
Pull request: 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).


Changed files:
filename: lib/internal/util/comparisons.js
patch: @@ -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]') {
-    return keyCheck(val1, val2, mode, memos, kNoIterator);
-  } else if (isDate(val1)) {
-    if (!isDate(val2)) {
+  }
+
+  let val1Tag;
+  if (val1[SymbolToStringTag] === undefined &&
+      (val1Tag = ObjectPrototypeToString(val1)) === '[object Object]') {
+    if (slowHasUnequalTag(val1Tag, 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 keyCheck(val1, val2, mode, memos, kNoIterator);
+  } else if (isSet(val1)) {
+    if (!isSet(val2) ||
+        (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size)) ||
+        hasUnequalTag(val1, val2)) {
       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,18 +897,16 @@ 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;
   }
   for (let i = 0; i < lenB; i++) {
     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) {