util,assert: improve comparison performance

This adds a smarter logic to compare object keys (including symbols)
and it also skips the object key comparison for (typed) arrays, if
possible.

Besides that it adds a fast path for empty objects, arrays, sets and
maps and fast paths for sets and maps with an unequal size.

On top of that a few functions are now safer to call by using
uncurryThis and by caching the actual function.

Overall, this is a significant performance boost for comparisons.

PR-URL: https://github.com/nodejs/node/pull/22258
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
Ruben Bridgewater 2018-08-10 19:28:17 +02:00
parent 525544b73d
commit d164d9d606
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
2 changed files with 205 additions and 118 deletions

View File

@ -5,32 +5,52 @@ const { isArrayBufferView } = require('internal/util/types');
const { internalBinding } = require('internal/bootstrap/loaders');
const { isDate, isMap, isRegExp, isSet } = internalBinding('types');
function objectToString(o) {
return Object.prototype.toString.call(o);
const ReflectApply = Reflect.apply;
function uncurryThis(func) {
return (thisArg, ...args) => ReflectApply(func, thisArg, args);
}
const kStrict = true;
const kLoose = false;
const kNoIterator = 0;
const kIsArray = 1;
const kIsSet = 2;
const kIsMap = 3;
const objectToString = uncurryThis(Object.prototype.toString);
const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable);
const objectKeys = Object.keys;
const getPrototypeOf = Object.getPrototypeOf;
const getOwnPropertySymbols = Object.getOwnPropertySymbols;
const objectIs = Object.is;
const numberIsNaN = Number.isNaN;
// Check if they have the same source and flags
function areSimilarRegExps(a, b) {
return a.source === b.source && a.flags === b.flags;
}
// For small buffers it's faster to compare the buffer in a loop. The c++
// barrier including the Uint8Array operation takes the advantage of the faster
// binary compare otherwise. The break even point was at about 300 characters.
function areSimilarTypedArrays(a, b, max) {
const len = a.byteLength;
if (len !== b.byteLength) {
function areSimilarFloatArrays(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
}
if (len < max) {
for (var offset = 0; offset < len; offset++) {
if (a[offset] !== b[offset]) {
return false;
}
for (var offset = 0; offset < a.byteLength; offset++) {
if (a[offset] !== b[offset]) {
return false;
}
return true;
}
return compare(new Uint8Array(a.buffer, a.byteOffset, len),
return true;
}
function areSimilarTypedArrays(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
}
return compare(new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0;
}
@ -66,8 +86,8 @@ function isObjectOrArrayTag(tag) {
// b) The same prototypes.
function strictDeepEqual(val1, val2, memos) {
if (typeof val1 !== 'object') {
return typeof val1 === 'number' && Number.isNaN(val1) &&
Number.isNaN(val2);
return typeof val1 === 'number' && numberIsNaN(val1) &&
numberIsNaN(val2);
}
if (typeof val2 !== 'object' || val1 === null || val2 === null) {
return false;
@ -78,21 +98,38 @@ function strictDeepEqual(val1, val2, memos) {
if (val1Tag !== val2Tag) {
return false;
}
if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) {
if (getPrototypeOf(val1) !== getPrototypeOf(val2)) {
return false;
}
if (val1Tag === '[object Array]') {
// Check for sparse arrays and general fast path
if (val1.length !== val2.length)
if (val1.length !== val2.length) {
return false;
// Skip testing the part below and continue with the keyCheck.
return keyCheck(val1, val2, true, memos);
}
const keys = objectKeys(val1);
if (keys.length !== objectKeys(val2).length) {
return false;
}
// Fast path for non sparse arrays (no key comparison for indices
// properties).
// See https://tc39.github.io/ecma262/#sec-ordinaryownpropertykeys
if (val1.length === keys.length) {
if (keys.length === 0 || keys[val1.length - 1] === `${val1.length - 1}`) {
return keyCheck(val1, val2, kStrict, memos, kIsArray, []);
}
} else if (keys.length > val1.length &&
keys[val1.length - 1] === `${val1.length - 1}`) {
const minimalKeys = keys.slice(val1.length);
return keyCheck(val1, val2, kStrict, memos, kIsArray, minimalKeys);
}
// Only set this to kIsArray in case the array is not sparse!
return keyCheck(val1, val2, kStrict, memos, kNoIterator, keys);
}
if (val1Tag === '[object Object]') {
// Skip testing the part below and continue with the keyCheck.
return keyCheck(val1, val2, true, memos);
return keyCheck(val1, val2, kStrict, memos, kNoIterator);
}
if (isDate(val1)) {
// TODO: Make these safe.
if (val1.getTime() !== val2.getTime()) {
return false;
}
@ -108,35 +145,52 @@ function strictDeepEqual(val1, val2, memos) {
return false;
}
} else if (isArrayBufferView(val1)) {
if (!areSimilarTypedArrays(val1, val2,
isFloatTypedArrayTag(val1Tag) ? 0 : 300)) {
if (!areSimilarTypedArrays(val1, val2)) {
return false;
}
// Buffer.compare returns true, so val1.length === val2.length
// if they both only contain numeric keys, we don't need to exam further
return keyCheck(val1, val2, true, memos, val1.length,
val2.length);
// if they both only contain numeric keys, we don't need to exam further.
const keys = objectKeys(val1);
if (keys.length !== objectKeys(val2).length) {
return false;
}
if (keys.length === val1.length) {
return keyCheck(val1, val2, kStrict, memos, kNoIterator, []);
}
// Only compare the special keys.
const minimalKeys = keys.slice(val1.length);
return keyCheck(val1, val2, kStrict, memos, kNoIterator, minimalKeys);
} else if (isSet(val1)) {
if (!isSet(val2) || val1.size !== val2.size) {
return false;
}
return keyCheck(val1, val2, kStrict, memos, kIsSet);
} else if (isMap(val1)) {
if (!isMap(val2) || val1.size !== val2.size) {
return false;
}
return keyCheck(val1, val2, kStrict, memos, kIsMap);
// TODO: Make the valueOf checks safe.
} else if (typeof val1.valueOf === 'function') {
const val1Value = val1.valueOf();
// Note: Boxed string keys are going to be compared again by Object.keys
if (val1Value !== val1) {
if (typeof val2.valueOf !== 'function') {
return false;
}
if (!innerDeepEqual(val1Value, val2.valueOf(), true))
return false;
// Fast path for boxed primitives
var lengthval1 = 0;
var lengthval2 = 0;
// Fast path for boxed primitive strings.
if (typeof val1Value === 'string') {
lengthval1 = val1.length;
lengthval2 = val2.length;
const keys = objectKeys(val1);
if (keys.length !== objectKeys(val2).length) {
return false;
}
const minimalKeys = keys.slice(val1.length);
return keyCheck(val1, val2, kStrict, memos, kNoIterator, minimalKeys);
}
return keyCheck(val1, val2, true, memos, lengthval1,
lengthval2);
}
}
return keyCheck(val1, val2, true, memos);
return keyCheck(val1, val2, kStrict, memos, kNoIterator);
}
function looseDeepEqual(val1, val2, memos) {
@ -150,33 +204,54 @@ function looseDeepEqual(val1, val2, memos) {
if (val2 === null || typeof val2 !== 'object') {
return false;
}
if (isDate(val1) && isDate(val2)) {
return val1.getTime() === val2.getTime();
}
if (isRegExp(val1) && isRegExp(val2)) {
return areSimilarRegExps(val1, val2);
}
if (val1 instanceof Error && val2 instanceof Error) {
if (val1.message !== val2.message || val1.name !== val2.name)
return false;
}
const val1Tag = objectToString(val1);
const val2Tag = objectToString(val2);
if (val1Tag === val2Tag) {
if (!isObjectOrArrayTag(val1Tag) && isArrayBufferView(val1)) {
return areSimilarTypedArrays(val1, val2,
isFloatTypedArrayTag(val1Tag) ?
Infinity : 300);
if (isObjectOrArrayTag(val1Tag)) {
return keyCheck(val1, val2, kLoose, memos, kNoIterator);
}
if (isArrayBufferView(val1)) {
if (isFloatTypedArrayTag(val1Tag)) {
return areSimilarFloatArrays(val1, val2);
}
return areSimilarTypedArrays(val1, val2);
}
if (isDate(val1) && isDate(val2)) {
return val1.getTime() === val2.getTime();
}
if (isRegExp(val1) && isRegExp(val2)) {
return areSimilarRegExps(val1, val2);
}
if (val1 instanceof Error && val2 instanceof Error) {
if (val1.message !== val2.message || val1.name !== val2.name)
return false;
}
// Ensure reflexivity of deepEqual with `arguments` objects.
// See https://github.com/nodejs/node-v0.x-archive/pull/7178
} else if (isArguments(val1Tag) || isArguments(val2Tag)) {
return false;
}
return keyCheck(val1, val2, false, memos);
if (isSet(val1)) {
if (!isSet(val2) || val1.size !== val2.size) {
return false;
}
return keyCheck(val1, val2, kLoose, memos, kIsSet);
} else if (isMap(val1)) {
if (!isMap(val2) || val1.size !== val2.size) {
return false;
}
return keyCheck(val1, val2, kLoose, memos, kIsMap);
} else if (isSet(val2) || isMap(val2)) {
return false;
}
return keyCheck(val1, val2, kLoose, memos, kNoIterator);
}
function keyCheck(val1, val2, strict, memos, lengthA, lengthB) {
function getEnumerables(val, keys) {
return keys.filter((k) => propertyIsEnumerable(val, k));
}
function keyCheck(val1, val2, strict, memos, iterationType, aKeys) {
// For all remaining Object pairs, including Array, objects and Maps,
// equivalence is determined by having:
// a) The same number of owned enumerable properties
@ -184,50 +259,59 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) {
// c) Equivalent values for every corresponding key/index
// d) For Sets and Maps, equal contents
// Note: this accounts for both named and indexed properties on Arrays.
var aKeys = Object.keys(val1);
var bKeys = Object.keys(val2);
var i;
if (arguments.length === 5) {
aKeys = objectKeys(val1);
const bKeys = objectKeys(val2);
// The pair must have the same number of owned properties.
if (aKeys.length !== bKeys.length)
return false;
// The pair must have the same number of owned properties.
if (aKeys.length !== bKeys.length) {
return false;
}
}
// Cheap key test
let i = 0;
for (; i < aKeys.length; i++) {
if (!hasOwnProperty(val2, aKeys[i])) {
return false;
}
}
if (strict) {
var symbolKeysA = Object.getOwnPropertySymbols(val1);
var symbolKeysB = Object.getOwnPropertySymbols(val2);
const symbolKeysA = getOwnPropertySymbols(val1);
if (symbolKeysA.length !== 0) {
symbolKeysA = symbolKeysA.filter((k) =>
propertyIsEnumerable.call(val1, k));
symbolKeysB = symbolKeysB.filter((k) =>
propertyIsEnumerable.call(val2, k));
if (symbolKeysA.length !== symbolKeysB.length)
let count = 0;
for (i = 0; i < symbolKeysA.length; i++) {
const key = symbolKeysA[i];
if (propertyIsEnumerable(val1, key)) {
if (!propertyIsEnumerable(val2, key)) {
return false;
}
aKeys.push(key);
count++;
} else if (propertyIsEnumerable(val2, key)) {
return false;
}
}
const symbolKeysB = getOwnPropertySymbols(val2);
if (symbolKeysA.length !== symbolKeysB.length &&
getEnumerables(val2, symbolKeysB).length !== count) {
return false;
} else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) =>
propertyIsEnumerable.call(val2, k)).length !== 0) {
return false;
}
if (lengthA !== undefined) {
if (aKeys.length !== lengthA || bKeys.length !== lengthB)
}
} else {
const symbolKeysB = getOwnPropertySymbols(val2);
if (symbolKeysB.length !== 0 &&
getEnumerables(val2, symbolKeysB).length !== 0) {
return false;
if (symbolKeysA.length === 0)
return true;
aKeys = [];
bKeys = [];
}
if (symbolKeysA.length !== 0) {
aKeys.push(...symbolKeysA);
bKeys.push(...symbolKeysB);
}
}
}
// Cheap key test:
const keys = {};
for (i = 0; i < aKeys.length; i++) {
keys[aKeys[i]] = true;
}
for (i = 0; i < aKeys.length; i++) {
if (keys[bKeys[i]] === undefined)
return false;
if (aKeys.length === 0 &&
(iterationType === kNoIterator ||
iterationType === kIsArray && val1.length === 0 ||
val1.size === 0)) {
return true;
}
// Use memos to handle cycles.
@ -254,7 +338,7 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) {
memos.val1.set(val1, memos.position);
memos.val2.set(val2, memos.position);
const areEq = objEquiv(val1, val2, strict, aKeys, memos);
const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType);
memos.val1.delete(val1);
memos.val2.delete(val2);
@ -267,7 +351,7 @@ function innerDeepEqual(val1, val2, strict, memos) {
if (val1 === val2) {
if (val1 !== 0)
return true;
return strict ? Object.is(val1, val2) : true;
return strict ? objectIs(val1, val2) : true;
}
// Check more closely if val1 and val2 are equal.
@ -297,7 +381,7 @@ function setHasLoosePrim(a, b, val) {
if (altValues === undefined)
return false;
var matches = 1;
let matches = 1;
for (var i = 0; i < altValues.length; i++) {
if (b.has(altValues[i])) {
matches--;
@ -310,19 +394,9 @@ function setHasLoosePrim(a, b, val) {
}
function setEquiv(a, b, strict, memo) {
// This code currently returns false for this pair of sets:
// assert.deepEqual(new Set(['1', 1]), new Set([1]))
//
// In theory, all the items in the first set have a corresponding == value in
// the second set, but the sets have different sizes. Its a silly case,
// and more evidence that deepStrictEqual should always be preferred over
// deepEqual.
if (a.size !== b.size)
return false;
// This is a lazily initiated Set of entries which have to be compared
// pairwise.
var set = null;
let set = null;
for (const val of a) {
// Note: Checking for the objects first improves the performance for object
// heavy sets but it is a minor slow down for primitives. As they are fast
@ -405,7 +479,7 @@ function mapHasLoosePrim(a, b, key1, memo, item1, item2) {
const setA = new Set();
const setB = new Set();
var keyCount = 1;
let keyCount = 1;
setA.add(item1);
if (b.has(key1)) {
@ -454,10 +528,7 @@ function mapHasEqualEntry(set, map, key1, item1, strict, memo) {
}
function mapEquiv(a, b, strict, memo) {
if (a.size !== b.size)
return false;
var set = null;
let set = null;
for (const [key, item1] of a) {
if (typeof key === 'object' && key !== null) {
@ -492,35 +563,44 @@ function mapEquiv(a, b, strict, memo) {
return true;
}
function objEquiv(a, b, strict, keys, memos) {
function objEquiv(a, b, strict, keys, memos, iterationType) {
// Sets and maps don't have their entries accessible via normal object
// properties.
if (isSet(a)) {
if (!isSet(b) || !setEquiv(a, b, strict, memos))
let i = 0;
if (iterationType === kIsSet) {
if (!setEquiv(a, b, strict, memos)) {
return false;
} else if (isMap(a)) {
if (!isMap(b) || !mapEquiv(a, b, strict, memos))
}
} else if (iterationType === kIsMap) {
if (!mapEquiv(a, b, strict, memos)) {
return false;
} else if (isSet(b) || isMap(b)) {
return false;
}
} else if (iterationType === kIsArray) {
for (; i < a.length; i++) {
if (!innerDeepEqual(a[i], b[i], strict, memos)) {
return false;
}
}
}
// The pair must have equivalent values for every corresponding key.
// Possibly expensive deep test:
for (var i = 0; i < keys.length; i++) {
for (i = 0; i < keys.length; i++) {
const key = keys[i];
if (!innerDeepEqual(a[key], b[key], strict, memos))
if (!innerDeepEqual(a[key], b[key], strict, memos)) {
return false;
}
}
return true;
}
function isDeepEqual(val1, val2) {
return innerDeepEqual(val1, val2, false);
return innerDeepEqual(val1, val2, kLoose);
}
function isDeepStrictEqual(val1, val2) {
return innerDeepEqual(val1, val2, true);
return innerDeepEqual(val1, val2, kStrict);
}
module.exports = {

View File

@ -901,3 +901,10 @@ assert.deepStrictEqual(obj1, obj2);
a.valueOf = undefined;
assertNotDeepOrStrict(a, new String(1));
}
// Basic array out of bounds check.
{
const arr = [1, 2, 3];
arr[2 ** 32] = true;
assertNotDeepOrStrict(arr, [1, 2, 3]);
}