assert: improve partialDeepStrictEqual performance

This implements fast paths for typed arrays, array buffers and sets
and maps that contain only objects as keys.

PR-URL: https://github.com/nodejs/node/pull/57509
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
Ruben Bridgewater 2025-03-20 00:50:56 +01:00 committed by GitHub
parent ea9be17872
commit 1fbe3351ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 55 additions and 24 deletions

View file

@ -7,7 +7,7 @@ const bench = common.createBenchmark(main, {
len: [1e2, 1e3],
strict: [0, 1],
arrayBuffer: [0, 1],
method: ['deepEqual', 'notDeepEqual', 'unequal_length'],
method: ['deepEqual', 'notDeepEqual', 'unequal_length', 'partial'],
}, {
combinationFilter: (p) => {
return p.strict === 1 || p.method === 'deepEqual';
@ -18,11 +18,16 @@ function main({ len, n, method, strict, arrayBuffer }) {
let actual = Buffer.alloc(len);
let expected = Buffer.alloc(len + Number(method === 'unequal_length'));
if (method === 'unequal_length') {
method = 'notDeepEqual';
}
if (method === 'partial') {
method = 'partialDeepStrictEqual';
} else if (strict) {
method = method.replace('eep', 'eepStrict');
}
for (let i = 0; i < len; i++) {
actual.writeInt8(i % 128, i);
expected.writeInt8(i % 128, i);
@ -33,10 +38,6 @@ function main({ len, n, method, strict, arrayBuffer }) {
expected[position] = expected[position] + 1;
}
if (strict) {
method = method.replace('eep', 'eepStrict');
}
const fn = assert[method];
if (arrayBuffer) {

View file

@ -4,12 +4,13 @@ const common = require('../common.js');
const assert = require('assert');
const bench = common.createBenchmark(main, {
n: [25],
n: [125],
size: [500],
extraProps: [0],
extraProps: [0, 1],
datasetName: [
'objects',
'sets',
'setsWithObjects',
'maps',
'circularRefs',
'typedArrays',
@ -31,17 +32,29 @@ function createObjects(length, extraProps, depth = 0) {
foo: 'yarp',
nope: {
bar: '123',
...extraProps ? { a: [1, 2, i] } : {},
...(extraProps ? { a: [1, 2, i] } : {}),
c: {},
b: !depth ? createObjects(2, extraProps, depth + 1) : [],
},
}));
}
function createSetsWithObjects(length, extraProps, depth = 0) {
return Array.from({ length }, (_, i) => new Set([
...(extraProps ? [{}] : []),
{
simple: 'object',
number: i,
},
['array', 'with', 'values'],
new Set([[], {}, { nested: i }]),
]));
}
function createSets(length, extraProps, depth = 0) {
return Array.from({ length }, (_, i) => new Set([
'yarp',
...extraProps ? ['123', 1, 2] : [],
...(extraProps ? ['123', 1, 2] : []),
i + 3,
null,
{
@ -56,7 +69,7 @@ function createSets(length, extraProps, depth = 0) {
function createMaps(length, extraProps, depth = 0) {
return Array.from({ length }, (_, i) => new Map([
...extraProps ? [['primitiveKey', 'primitiveValue']] : [],
...(extraProps ? [['primitiveKey', 'primitiveValue']] : []),
[42, 'numberKey'],
['objectValue', { a: 1, b: i }],
['arrayValue', [1, 2, i]],
@ -114,16 +127,23 @@ function createTypedArrays(length, extraParts) {
}
function createArrayBuffers(length, extra) {
return Array.from({ length }, (_, n) => new ArrayBuffer(n + extra ? 1 : 0));
return Array.from({ length }, (_, n) => {
const buffer = Buffer.alloc(n + (extra ? 1 : 0));
for (let i = 0; i < n; i++) {
buffer.writeInt8(i % 128, i);
}
return buffer.buffer;
});
}
function createDataViewArrayBuffers(length, extra) {
return Array.from({ length }, (_, n) => new DataView(new ArrayBuffer(n + extra ? 1 : 0)));
return createArrayBuffers(length, extra).map((buffer) => new DataView(buffer));
}
const datasetMappings = {
objects: createObjects,
sets: createSets,
setsWithObjects: createSetsWithObjects,
maps: createMaps,
circularRefs: createCircularRefs,
typedArrays: createTypedArrays,

View file

@ -243,7 +243,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
TypedArrayPrototypeGetSymbolToStringTag(val2)) {
return false;
}
if (mode === kPartial) {
if (mode === kPartial && val1.byteLength !== val2.byteLength) {
if (!isPartialArrayBufferView(val1, val2)) {
return false;
}
@ -280,7 +280,7 @@ function innerDeepEqual(val1, val2, mode, memos) {
if (!isAnyArrayBuffer(val2)) {
return false;
}
if (mode !== kPartial) {
if (mode !== kPartial || val1.byteLength === val2.byteLength) {
if (!areEqualArrayBuffers(val1, val2)) {
return false;
}
@ -546,11 +546,8 @@ 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) {
if (mode !== kLoose && set.size === a.size) {
for (const val of a) {
if (!setHasEqualElement(set, val, mode, memo)) {
return false;
@ -558,6 +555,9 @@ function setObjectEquiv(a, b, mode, set, memo) {
}
return true;
}
if (mode === kPartial) {
return partialObjectSetEquiv(a, b, mode, set, memo);
}
for (const val of a) {
// Primitive values have already been handled above.
@ -639,11 +639,8 @@ 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) {
if (mode !== kLoose && set.size === a.size) {
for (const { 0: key1, 1: item1 } of a) {
if (!mapHasEqualEntry(set, b, key1, item1, mode, memo)) {
return false;
@ -651,6 +648,9 @@ function mapObjectEquivalence(a, b, mode, set, memo) {
}
return true;
}
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))

View file

@ -280,6 +280,11 @@ describe('Object Comparison Tests', () => {
[{ a: 1 }, 'value1'],
]),
},
{
description: 'throws for Maps with mixed unequal entries',
actual: new Map([[{ a: 2 }, 1], [1, 1], [{ b: 1 }, 1], [[], 1], [2, 1], [{ a: 1 }, 1]]),
expected: new Map([[{ a: 1 }, 1], [[], 1], [2, 1], [{ a: 1 }, 1]]),
},
{
description: 'throws for sets with different object values',
actual: new Set([
@ -494,6 +499,11 @@ describe('Object Comparison Tests', () => {
actual: new Float32Array([+0.0]),
expected: new Float32Array([-0.0]),
},
{
description: 'throws when comparing two Uint8Array objects with non-matching entries',
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
expected: { typedArray: new Uint8Array([1, 333, 2, 4]) },
},
{
description: 'throws when comparing two different urls',
actual: new URL('http://foo'),
@ -713,7 +723,7 @@ describe('Object Comparison Tests', () => {
{
description: 'compares two Uint8Array objects',
actual: { typedArray: new Uint8Array([1, 2, 3, 4, 5]) },
expected: { typedArray: new Uint8Array([1, 2, 3]) },
expected: { typedArray: new Uint8Array([1, 2, 3, 5]) },
},
{
description: 'compares two Int16Array objects',