Skip to content

Commit c74410b

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

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) {
@@ -445,7 +448,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
445448
memos.c = val1;
446449
memos.d = val2;
447450
memos.deep = true;
448-
const result = objEquiv(val1, val2, mode, keys2, memos, iterationType);
451+
const result = objEquiv(val1, val2, mode, keys1, keys2, memos, iterationType);
449452
memos.deep = false;
450453
return result;
451454
}
@@ -465,7 +468,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
465468
return originalSize === set.size;
466469
}
467470

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

470473
set.delete(val1);
471474
set.delete(val2);
@@ -581,16 +584,34 @@ function setEquiv(a, b, mode, memo) {
581584
// This is a lazily initiated Set of entries which have to be compared
582585
// pairwise.
583586
let set = null;
584-
for (const val of b) {
587+
const iteratorB = b.values();
588+
for (const val of iteratorB) {
585589
if (!a.has(val)) {
586590
if ((typeof val !== 'object' || val === null) &&
587591
(mode !== kLoose || !setMightHaveLoosePrim(a, b, val))) {
588592
return false;
589593
}
590594

591595
if (set === null) {
592-
if (a.size === 1) {
593-
return innerDeepEqual(a.values().next().value, val, mode, memo);
596+
if (a.size < 3) {
597+
const iteratorA = a.values();
598+
const firstA = iteratorA.next().value;
599+
const first = innerDeepEqual(firstA, val, mode, memo);
600+
if (first) {
601+
if (b.size === 1) { // Partial mode && a.size === 1
602+
return true;
603+
}
604+
const secondA = iteratorA.next().value;
605+
return b.has(secondA) || innerDeepEqual(secondA, iteratorB.next().value, mode, memo);
606+
}
607+
if (a.size === 1) {
608+
return false;
609+
}
610+
return innerDeepEqual(iteratorA.next().value, val, mode, memo) && (
611+
b.size === 1 || // Partial mode
612+
b.has(firstA) || // Primitive or reference equal
613+
innerDeepEqual(firstA, iteratorB.next().value, mode, memo)
614+
);
594615
}
595616
set = new SafeSet();
596617
}
@@ -770,11 +791,28 @@ function sparseArrayEquiv(a, b, mode, memos, i) {
770791
return true;
771792
}
772793

773-
function objEquiv(a, b, mode, keys2, memos, iterationType) {
794+
function objEquiv(a, b, mode, keys1, keys2, memos, iterationType) {
774795
// The pair must have equivalent values for every corresponding key.
775796
if (keys2.length > 0) {
776-
for (const key of keys2) {
777-
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
797+
let i = 0;
798+
// Ordered keys
799+
if (keys1 !== undefined) {
800+
for (; i < keys2.length; i++) {
801+
const key = keys2[i];
802+
if (keys1[i] !== key) {
803+
break;
804+
}
805+
if (!innerDeepEqual(a[key], b[key], mode, memos)) {
806+
return false;
807+
}
808+
}
809+
}
810+
// Unordered keys
811+
for (; i < keys2.length; i++) {
812+
const key = keys2[i];
813+
const descriptor = ObjectGetOwnPropertyDescriptor(a, key);
814+
if (!descriptor?.enumerable ||
815+
!innerDeepEqual(descriptor.value !== undefined ? descriptor.value : a[key], b[key], mode, memos)) {
778816
return false;
779817
}
780818
}

0 commit comments

Comments
 (0)