Skip to content

Commit 2b52ea7

Browse files
committed
assert,util: improve deep object comparison performance
This improves the performance for almost all objects when comparing them deeply.
1 parent f46895a commit 2b52ea7

File tree

4 files changed

+74
-37
lines changed

4 files changed

+74
-37
lines changed

benchmark/assert/deepequal-prims-and-objs-big-loop.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const primValues = {
1414
'number': 1_000,
1515
'boolean': true,
1616
'object': { property: 'abcdef' },
17-
'object_other_property': { property: 'abcdef' },
1817
'array': [1, 2, 3],
1918
'set_object': new Set([[1]]),
2019
'set_simple': new Set([1, 2, 3]),

benchmark/assert/deepequal-set.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } =
66

77
const bench = common.createBenchmark(main, {
88
n: [1e3],
9-
len: [5e2],
9+
len: [2, 5e2],
1010
strict: [0, 1],
1111
method: [
1212
'deepEqual_primitiveOnly',

benchmark/assert/partial-deep-equal.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ function createSets(length, extraProps, depth = 0) {
6262
number: i,
6363
},
6464
['array', 'with', 'values'],
65-
!depth ? new Set([1, 2, { nested: i }]) : new Set(),
65+
!depth ? new Set([1, { nested: i }]) : new Set(),
6666
!depth ? createSets(2, extraProps, depth + 1) : null,
6767
]));
6868
}

lib/internal/util/comparisons.js

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
DatePrototypeGetTime,
1010
Error,
1111
NumberPrototypeValueOf,
12+
ObjectGetOwnPropertyDescriptor,
1213
ObjectGetOwnPropertySymbols: getOwnSymbols,
1314
ObjectGetPrototypeOf,
1415
ObjectIs,
@@ -192,7 +193,10 @@ function innerDeepEqual(val1, val2, mode, memos) {
192193
typeof val1 !== 'object' ||
193194
val1 === null ||
194195
val2 === null ||
195-
(mode === kStrict && ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))) {
196+
(mode === kStrict &&
197+
(val1.constructor !== val2.constructor ||
198+
(val1.constructor === undefined &&
199+
ObjectGetPrototypeOf(val1) !== ObjectGetPrototypeOf(val2))))) {
196200
return false;
197201
}
198202
} else {
@@ -316,6 +320,10 @@ function innerDeepEqual(val1, val2, mode, memos) {
316320
isNativeError(val2) ||
317321
val2 instanceof Error) {
318322
return false;
323+
} else if (isURL(val1)) {
324+
if (!isURL(val2) || val1.href !== val2.href) {
325+
return false;
326+
}
319327
} else if (isKeyObject(val1)) {
320328
if (!isKeyObject(val2) || !val1.equals(val2)) {
321329
return false;
@@ -332,10 +340,6 @@ function innerDeepEqual(val1, val2, mode, memos) {
332340
}
333341
} else if (isWeakMap(val1) || isWeakSet(val1)) {
334342
return false;
335-
} else if (isURL(val1)) {
336-
if (!isURL(val2) || val1.href !== val2.href) {
337-
return false;
338-
}
339343
}
340344

341345
return keyCheck(val1, val2, mode, memos, kNoIterator);
@@ -345,6 +349,21 @@ function getEnumerables(val, keys) {
345349
return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key));
346350
}
347351

352+
function partialSymbolEquiv(val1, val2, keys2) {
353+
const symbolKeys = getOwnSymbols(val2);
354+
if (symbolKeys.length !== 0) {
355+
for (const key of symbolKeys) {
356+
if (hasEnumerable(val2, key)) {
357+
if (!hasEnumerable(val1, key)) {
358+
return false;
359+
}
360+
ArrayPrototypePush(keys2, key);
361+
}
362+
}
363+
}
364+
return true;
365+
}
366+
348367
function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
349368
// For all remaining Object pairs, including Array, objects and Maps,
350369
// equivalence is determined by having:
@@ -358,31 +377,15 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
358377
if (keys2 === undefined) {
359378
keys2 = ObjectKeys(val2);
360379
}
361-
362-
// Cheap key test
363-
if (keys2.length > 0) {
364-
for (const key of keys2) {
365-
if (!hasEnumerable(val1, key)) {
366-
return false;
367-
}
368-
}
369-
}
380+
let keys1;
370381

371382
if (!isArrayLikeObject) {
372383
// The pair must have the same number of owned properties.
373384
if (mode === kPartial) {
374-
const symbolKeys = getOwnSymbols(val2);
375-
if (symbolKeys.length !== 0) {
376-
for (const key of symbolKeys) {
377-
if (hasEnumerable(val2, key)) {
378-
if (!hasEnumerable(val1, key)) {
379-
return false;
380-
}
381-
ArrayPrototypePush(keys2, key);
382-
}
383-
}
385+
if (!partialSymbolEquiv(val1, val2, keys2)) {
386+
return false;
384387
}
385-
} else if (keys2.length !== ObjectKeys(val1).length) {
388+
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
386389
return false;
387390
} else if (mode === kStrict) {
388391
const symbolKeysA = getOwnSymbols(val1);
@@ -431,7 +434,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
431434
d: undefined,
432435
deep: false,
433436
};
434-
return objEquiv(val1, val2, mode, keys2, memos, iterationType);
437+
return objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
435438
}
436439

437440
if (memos.set === undefined) {
@@ -442,7 +445,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
442445
memos.c = val1;
443446
memos.d = val2;
444447
memos.deep = true;
445-
const result = objEquiv(val1, val2, mode, keys2, memos, iterationType);
448+
const result = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
446449
memos.deep = false;
447450
return result;
448451
}
@@ -462,7 +465,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
462465
return true;
463466
}
464467

465-
const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType);
468+
const areEq = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
466469

467470
set.delete(val1);
468471
set.delete(val2);
@@ -578,16 +581,34 @@ function setEquiv(a, b, mode, memo) {
578581
// This is a lazily initiated Set of entries which have to be compared
579582
// pairwise.
580583
let set = null;
581-
for (const val of b) {
584+
const iteratorB = b.values();
585+
for (const val of iteratorB) {
582586
if (!a.has(val)) {
583587
if ((typeof val !== 'object' || val === null) &&
584588
(mode !== kLoose || !setMightHaveLoosePrim(a, b, val))) {
585589
return false;
586590
}
587591

588592
if (set === null) {
589-
if (a.size === 1) {
590-
return innerDeepEqual(a.values().next().value, val, mode, memo);
593+
if (a.size < 3) {
594+
const iteratorA = a.values();
595+
const firstA = iteratorA.next().value;
596+
const first = innerDeepEqual(firstA, val, mode, memo);
597+
if (first) {
598+
if (b.size === 1) { // Partial mode && a.size === 1
599+
return true;
600+
}
601+
const secondA = iteratorA.next().value;
602+
return b.has(secondA) || innerDeepEqual(secondA, iteratorB.next().value, mode, memo);
603+
}
604+
if (a.size === 1) {
605+
return false;
606+
}
607+
return innerDeepEqual(iteratorA.next().value, val, mode, memo) && (
608+
b.size === 1 || // Partial mode
609+
b.has(firstA) || // Primitive or reference equal
610+
innerDeepEqual(firstA, iteratorB.next().value, mode, memo)
611+
);
591612
}
592613
set = new SafeSet();
593614
}
@@ -767,11 +788,28 @@ function sparseArrayEquiv(a, b, mode, memos, i) {
767788
return true;
768789
}
769790

770-
function objEquiv(a, b, mode, keys2, memos, iterationType) {
791+
function objEquiv(a, b, mode, keys1, keys2, memos, iterationType) {
771792
// The pair must have equivalent values for every corresponding key.
772793
if (keys2.length > 0) {
773-
for (const key of keys2) {
774-
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
794+
let i = 0;
795+
// Ordered keys
796+
if (keys1 !== undefined) {
797+
for (; i < keys2.length; i++) {
798+
const key = keys2[i];
799+
if (keys1[i] !== key) {
800+
break;
801+
}
802+
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
803+
return false;
804+
}
805+
}
806+
}
807+
// Unordered keys
808+
for (; i < keys2.length; i++) {
809+
const key = keys2[i];
810+
const descriptor = ObjectGetOwnPropertyDescriptor(a, key);
811+
if (!descriptor?.enumerable ||
812+
!innerDeepEqual(descriptor.value !== undefined ? descriptor.value : a[key], b[key], mode, memos)) {
775813
return false;
776814
}
777815
}

0 commit comments

Comments
 (0)