perf(assert): add fast path for primitive keyed collections in equal() (#5913)

* feat(assert): add fast path for primitive keyed collections

* remove fix for #5912 (to be redone as separate pr)

* add extra-fast-path with Set#symmetricDifference

* Changes from code review
This commit is contained in:
lionel-rowe 2024-09-07 08:11:42 +08:00 committed by GitHub
parent 70f981e605
commit 2fe19de48f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 2 deletions

View File

@ -1,7 +1,14 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible. // This module is browser compatible.
function isKeyedCollection(x: unknown): x is Set<unknown> {
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>)); type KeyedCollection = Set<unknown> | Map<unknown, unknown>;
function isKeyedCollection(x: unknown): x is KeyedCollection {
return x instanceof Set || x instanceof Map;
}
function getValFromKeyedCollection(kc: KeyedCollection, key: unknown) {
return kc instanceof Map ? kc.get(key) : key;
} }
function constructorsEqual(a: object, b: object) { function constructorsEqual(a: object, b: object) {
@ -82,12 +89,41 @@ export function equal(c: unknown, d: unknown): boolean {
return false; return false;
} }
const aKeys = [...a.keys()];
const primitiveKeysFastPath = aKeys.every((k) => {
return typeof k === "string" ||
typeof k === "number" ||
typeof k === "boolean" ||
typeof k === "bigint" ||
typeof k === "symbol" ||
k == null;
});
if (primitiveKeysFastPath) {
if (a instanceof Set) {
return a.symmetricDifference(b).size === 0;
}
for (const key of aKeys) {
if (
!b.has(key) ||
!compare(
getValFromKeyedCollection(a, key),
getValFromKeyedCollection(b, key),
)
) {
return false;
}
}
return true;
}
let unmatchedEntries = a.size; let unmatchedEntries = a.size;
for (const [aKey, aValue] of a.entries()) { for (const [aKey, aValue] of a.entries()) {
for (const [bKey, bValue] of b.entries()) { for (const [bKey, bValue] of b.entries()) {
/* Given that Map keys can be references, we need /* Given that Map keys can be references, we need
* to ensure that they are also deeply equal */ * to ensure that they are also deeply equal */
if ( if (
(aKey === aValue && bKey === bValue && compare(aKey, bKey)) || (aKey === aValue && bKey === bValue && compare(aKey, bKey)) ||
(compare(aKey, bKey) && compare(aValue, bValue)) (compare(aKey, bKey) && compare(aValue, bValue))

View File

@ -276,3 +276,31 @@ Deno.test("equal() WeakMap, WeakRef and WeakSet", () => {
assertFalse(equal(new WeakSet(), { constructor: WeakSet })); assertFalse(equal(new WeakSet(), { constructor: WeakSet }));
assertFalse(equal(new WeakRef({}), { constructor: WeakRef })); assertFalse(equal(new WeakRef({}), { constructor: WeakRef }));
}); });
Deno.test("equal() fast path for primitive keyed collections", () => {
const arr = Array.from({ length: 10_000 }, (_, i) => i);
const set1 = new Set(arr);
const set2 = new Set(arr);
assert(equal(set1, set2));
const map1 = new Map(arr.map((v) => [v, v]));
const map2 = new Map(arr.map((v) => [v, v]));
assert(equal(map1, map2));
const set3 = new Set(arr);
const set4 = new Set(arr.with(-1, -1));
assertFalse(equal(set3, set4));
// entries [...] 9998 => 9998, 9999 => 9999
const map3 = new Map(arr.map((v) => [v, v]));
// entries [...] 9998 => 9998, -1 => -1
const map4 = new Map(arr.with(-1, -1).map((v) => [v, v]));
assertFalse(equal(map3, map4));
// entries [...] 9998 => 9998, 9999 => 9999
const map5 = new Map(arr.map((v, i) => [i, v]));
// entries [...] 9998 => 9998, 9999 => -1
const map6 = new Map(arr.with(-1, -1).map((v, i) => [i, v]));
assertFalse(equal(map5, map6));
});