-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
assert: improve partialDeepStrictEqual performance and add benchmark #56555
base: main
Are you sure you want to change the base?
Conversation
Review requested:
|
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #56555 +/- ##
==========================================
+ Coverage 89.17% 89.21% +0.04%
==========================================
Files 662 663 +1
Lines 191672 191987 +315
Branches 36884 36928 +44
==========================================
+ Hits 170922 171289 +367
+ Misses 13614 13560 -54
- Partials 7136 7138 +2
|
lib/assert.js
Outdated
@@ -514,17 +524,15 @@ function partiallyCompareArrays(actual, expected, comparedObjects) { | |||
return false; | |||
} | |||
|
|||
if (isDeepEqual === undefined) lazyLoadComparison(); | |||
if (typeof isDeepStrictEqual === 'undefined') lazyLoadComparison(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would keep the former part. Using typeof
should not change the speed.
We can also keep it consistent with isDeepEqual
.
if (typeof isDeepStrictEqual === 'undefined') lazyLoadComparison(); | |
if (isDeepEqual === undefined) lazyLoadComparison(); |
lib/assert.js
Outdated
if (expectedItem === 0) { | ||
const zeroKey = getZeroKey(expectedItem); | ||
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey)?.count || 0) + 1); | ||
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) || 0) + 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) || 0) + 1); | |
expectedCounts.set(zeroKey, (expectedCounts.get(zeroKey) ?? 0) + 1); |
lib/assert.js
Outdated
@@ -554,6 +559,7 @@ function partiallyCompareArrays(actual, expected, comparedObjects) { | |||
} else { | |||
expectedCounts.set(zeroKey, count - 1); | |||
} | |||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The continue
won't be needed, since the if statement will end and all other operations in the loop are also done. It won't impact the performance anymore and it is normally easier to read without continue
statements.
continue; |
lib/assert.js
Outdated
for (const actualItem of actualIterator) { | ||
actualMap.set(actualItem, (actualMap.get(actualItem) || 0) + 1); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the count of each item is always exactly 1. The reason is, that it's either a primitive or an object and an object reference is unique as well. Otherwise the entry would be overwritten while setting the value.
lib/assert.js
Outdated
|
||
for (const expectedItem of expectedIterator) { | ||
let foundMatch = false; | ||
for (const { 0: actualItem, 1: count } of actualMapIterator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only have to look for entries that are not primitives. That way we safe a lot of iterations. You can look at the code how it's down in the set comparison of isDeepStrictEqual()
. It does exactly that. It can even be a bit simpler, because this does not require the non-strict checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BridgeAR very good point, I didn't think about it!
ac146c0
to
762399f
Compare
Is anything else needed here? |
@puskin94 Can you share the results of your benchmarking? |
of course 😄 ➜ node git:(partial-deep-strict-equal-perf) ✗ ./node benchmark/assert/partial-deep-strict-equal.js
assert/partial-deep-strict-equal.js datasetName="objects" size=10000 n=25: 15.980221170313582
assert/partial-deep-strict-equal.js datasetName="sets" size=10000 n=25: 13.736693007920234
assert/partial-deep-strict-equal.js datasetName="maps" size=10000 n=25: 14.897881357870947
assert/partial-deep-strict-equal.js datasetName="arrayBuffers" size=10000 n=25: 174.68530442408002 |
lib/assert.js
Outdated
// Create a map for faster lookups | ||
const actualMap = new SafeMap(); | ||
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use a Map
if we only store true
in it?
// Create a map for faster lookups | |
const actualMap = new SafeMap(); | |
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual); | |
const actualSet = new SafeSet( | |
FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual), | |
); |
762399f
to
d061759
Compare
lib/assert.js
Outdated
|
||
for (const expectedItem of safeExpected) { | ||
// Check if the item is a zero or a -0, as these need to be handled separately | ||
for (const expectedItem of new SafeArrayIterator(expected)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're looking for performance, we should probably be using a classic for(;;)
loop. I'd recommend rolling back this change (i.e. put back the safeExpected
assignment, because it has nothing to do with performance) for this PR, and make a new one switching to a for(;;)
loop on which we can run benchmarks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you do this tho you would have to call the ArrayFrom
primordial. The performance enhancement was to adjust the "low hanging fruit" stuff I could get, "structure" wise, without influencing readability. IMHO the current implementation is better, but happy to change if you don't agree!
reverted the assignment tho
d061759
to
f1f220c
Compare
if (isPrimitive(expectedItem)) { | ||
if (actualSet.has(expectedItem)) { | ||
actualSet.delete(expectedItem); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be improved further by just adding entries that are not detected. So we just lazily allocate the set for objects that are not reference identical on the other side.
This is how it's implemented in the current deepStrictEqual comparison.
now that #54630 has landed, I took the liberty to review the code I wrote to look for improvements that would squeeze some more performance out of the comparison mechanism
partialDeepStrictEqual
Refs: #54630