Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

puskin94
Copy link
Contributor

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

  • simplified some logic
  • reviewed loops and introduced more breaking points to make them run for less cycles
  • added a benchmark file to test future possible regressions when it comes to partialDeepStrictEqual

Refs: #54630

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/performance

@nodejs-github-bot nodejs-github-bot added assert Issues and PRs related to the assert subsystem. needs-ci PRs that need a full CI run. labels Jan 10, 2025
Copy link

codecov bot commented Jan 10, 2025

Codecov Report

Attention: Patch coverage is 57.74648% with 30 lines in your changes missing coverage. Please review.

Project coverage is 90.23%. Comparing base (6b3937a) to head (4502b76).
Report is 434 commits behind head on main.

Files with missing lines Patch % Lines
lib/assert.js 57.74% 30 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #56555      +/-   ##
==========================================
+ Coverage   89.17%   90.23%   +1.05%     
==========================================
  Files         662      630      -32     
  Lines      191672   184968    -6704     
  Branches    36884    36192     -692     
==========================================
- Hits       170922   166904    -4018     
+ Misses      13614    11084    -2530     
+ Partials     7136     6980     -156     
Files with missing lines Coverage Δ
lib/assert.js 96.50% <57.74%> (-2.54%) ⬇️

... and 236 files with indirect coverage changes

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from ac146c0 to 762399f Compare January 13, 2025 15:05
@puskin94 puskin94 requested a review from BridgeAR January 17, 2025 08:44
@puskin94
Copy link
Contributor Author

Is anything else needed here?

@lemire
Copy link
Member

lemire commented Jan 26, 2025

@puskin94 Can you share the results of your benchmarking?

@puskin94
Copy link
Contributor Author

puskin94 commented Jan 26, 2025

@puskin94 Can you share the results of your benchmarking?

of course 😄

➜  node git:(partial-deep-strict-equal-perf) ./node --no-warnings benchmark/assert/partial-deep-strict-equal.js 
assert/partial-deep-strict-equal.js datasetName="objects" size=1000 n=50: 169.87881572367604
assert/partial-deep-strict-equal.js datasetName="sets" size=1000 n=50: 141.24912248982653
assert/partial-deep-strict-equal.js datasetName="maps" size=1000 n=50: 151.36189759383777
assert/partial-deep-strict-equal.js datasetName="arrayBuffers" size=1000 n=50: 14.16071061277997
assert/partial-deep-strict-equal.js datasetName="dataViewArrayBuffers" size=1000 n=50: 16.398050206238278

lib/assert.js Outdated
Comment on lines 482 to 484
// Create a map for faster lookups
const actualMap = new SafeMap();
const actualIterator = FunctionPrototypeCall(SafeSet.prototype[SymbolIterator], actual);
Copy link
Contributor

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?

Suggested change
// 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),
);

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from 762399f to d061759 Compare January 27, 2025 11:08
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)) {
Copy link
Contributor

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

Copy link
Contributor Author

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

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from d061759 to f1f220c Compare January 27, 2025 12:25
lib/assert.js Outdated
Comment on lines 489 to 491
if (isPrimitive(expectedItem)) {
if (actualSet.has(expectedItem)) {
actualSet.delete(expectedItem);
Copy link
Member

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.

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from f1f220c to e932899 Compare January 29, 2025 08:00
@puskin94
Copy link
Contributor Author

puskin94 commented Feb 7, 2025

Is anything else needed here?

@puskin94
Copy link
Contributor Author

is any change required here?

@aduh95 aduh95 added author ready PRs that have at least one approval, no pending requests for changes, and a CI started. request-ci Add this label to start a Jenkins CI on a PR. labels Feb 21, 2025
@aduh95 aduh95 requested a review from BridgeAR February 21, 2025 16:32
@aduh95
Copy link
Contributor

aduh95 commented Feb 21, 2025

@BridgeAR can you clarify whether you're still blocking?

@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Feb 21, 2025
@nodejs-github-bot
Copy link
Collaborator

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply. I guess this improves a couple of cases, so that's great! :)
I believe there is more room for improvement by reusing the existing implementation. That could be used for a follow-up PR.
Before landing: could we run the benchmark to see the improvement as percentage? :)

@BridgeAR BridgeAR dismissed their stale review February 27, 2025 16:20

Just did a new review

@aduh95
Copy link
Contributor

aduh95 commented Feb 28, 2025

Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1664/

Results
                                                                                      confidence improvement accuracy (*)   (**)  (***)
assert/partial-deep-strict-equal.js datasetName='arrayBuffers' size=1000 n=50                ***     -0.79 %       ±0.38% ±0.51% ±0.67%
assert/partial-deep-strict-equal.js datasetName='dataViewArrayBuffers' size=1000 n=50         **      0.64 %       ±0.45% ±0.60% ±0.78%
assert/partial-deep-strict-equal.js datasetName='maps' size=1000 n=50                        ***      0.56 %       ±0.26% ±0.35% ±0.46%
assert/partial-deep-strict-equal.js datasetName='objects' size=1000 n=50                     ***     -0.49 %       ±0.28% ±0.37% ±0.48%
assert/partial-deep-strict-equal.js datasetName='sets' size=1000 n=50                          *      0.34 %       ±0.26% ±0.35% ±0.45%

Be aware that when doing many comparisons the risk of a false-positive
result increases. In this case, there are 5 comparisons, you can thus
expect the following amount of false-positive results:
  0.25 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.05 false positives, when considering a   1% risk acceptance (**, ***),
  0.01 false positives, when considering a 0.1% risk acceptance (***)

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from e932899 to a07cf19 Compare February 28, 2025 10:43
@puskin94
Copy link
Contributor Author

I must have messed up something with my local codebase. I just pushed again with new performance improvements, this should make some difference

@aduh95
Copy link
Contributor

aduh95 commented Feb 28, 2025

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from a07cf19 to b53b85c Compare February 28, 2025 11:21
@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from b53b85c to 989b529 Compare February 28, 2025 11:43
@aduh95
Copy link
Contributor

aduh95 commented Feb 28, 2025

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from 989b529 to 2e4fbdb Compare February 28, 2025 13:27
@BridgeAR
Copy link
Member

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not mistaken, this mainly improves the performance for sets and maps that have mixed entries (primitives + objects). The current benchmark has no such entry.

I also guess that the primordial usage of the iterator is costly. Could you check if that's the case?

lib/assert.js Outdated
Comment on lines 777 to 779
function isPrimitive(value) {
return typeof value !== 'object' || value === null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be inlined again. I guess that is where the performance degradation in the benchmark is coming from.

@puskin94 puskin94 force-pushed the partial-deep-strict-equal-perf branch from 2e4fbdb to 4502b76 Compare March 3, 2025 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem. author ready PRs that have at least one approval, no pending requests for changes, and a CI started. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants