-
Notifications
You must be signed in to change notification settings - Fork 30k
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 deepEqual perf for large input #12849
Conversation
Also, I found some weird behaviour while doing this: a = {};
assert.deepStrictEqual([a,a], [a,{}]); This does not throw, but I think it should; do you agree? Would you consider changing it semver-major? If not, I could do it in this PR, otherwise I’ll just wait until after this to avoid conflicts. |
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.
LGTM if the CI passes.
lib/assert.js
Outdated
const actualIndex = memos.actual.indexOf(actual); | ||
if (actualIndex !== -1) { | ||
if (actualIndex === memos.expected.indexOf(expected)) { | ||
memos = memos || { |
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.
How about instead doing if (!memos) { memos = ... }
?
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.
Sure, why not. Updated
lib/assert.js
Outdated
if (actualIndex !== -1) { | ||
if (actualIndex === memos.expected.indexOf(expected)) { | ||
if (!memos) { | ||
memors = { |
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.
memos
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.
fixed, sorry. 😄
lib/assert.js
Outdated
} else { | ||
memos.actual.map.set(actual, memos.actual.position++); | ||
} | ||
if (memos.expected.map.get(expected) === undefined) { |
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.
!memos.expected.map.has(expected)
?
776351a
to
788ac8b
Compare
7664b48
to
a60f489
Compare
if (actualIndex === memos.expected.indexOf(expected)) { | ||
if (!memos) { | ||
memos = { | ||
actual: { map: new Map(), position: 0 }, |
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.
How about a WeakMap
?
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.
@joyeecheung That thought occurred to me and I couldn’t really find a difference … would there be one? Is that going to be more performant?
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.
@addaleax I think the GC pressure would be smaller after we leave the function because the references don't have be traversed?
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.
@joyeecheung ok, updated :)
@addaleax I keep being surprised by your energy and dedication 🥇 |
@refack That seems like a completely different issue, though? :D |
It fit under the title "assert: improve deepEqual perf for large input". No need to sort keys of arrays. It is an O(n*log(n)) operation. |
@refack Arrays can have extra non-indexed properties, too ;) This might not be as easy as you think. If you think you can improve the situation, feel free to experiment and see if you can get a PR together, and I’ll be happy to review. |
Challenge accepted 🖖 |
Imo, no. |
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.
CI again, since the last one only ran linux(?): https://ci.nodejs.org/job/node-test-pull-request/7899/
@joyeecheung Looks like doing this with |
Sorry, huh – what do memory addresses have to do with this? It’s not about the left and the right side of To have a more explicit example: a = {}; b = {};
assert.deepStrictEqual({key1:a,key2:a},{key1:a,key2:b}); should throw imo, because |
|
||
const actualPosition = memos.actual.map.get(actual); | ||
if (actualPosition !== undefined) { | ||
if (actualPosition === memos.expected.map.get(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.
Changing from Array.prototype.indexOf (which uses Strict Equality Comparison) to Map.prototype.get (which uses SameValueZero) can have subtle nuances that might result in incompatibilities, especially around NaN. Not sure if we need to care about it though.
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 don’t think that actually affects the logic here – if it matters at all, it should only affect cases where non-primitives compare deep-equal to primitives, but that doesn’t seem to happen anyway (e.g. assert.deepEqual([0], 0)
throws even though [0] == 0
).
One more reason to consider the implementation of IMO, that should not throw (so the current behavior is correct). The So, confusingly, the "strict" part doesn't apply to objects (or arrays or--ideally at least--Maps or Sets). It does mean "don't cast a string to a number" though. Whee! But I do agree it's confusing and unfortunate. Even if you disagree, I definitely wouldn't fix it in this PR because I predict a CITGM run will not be kind to the additional change. |
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.
Change looks good to me if CI is green and if benchmark/assert
doesn't show any perf regressions. (Maybe add a benchmark to show the benefit of this change if existing benchmarks don't already show it?)
CI (linux only): https://ci.nodejs.org/job/node-test-commit-linux/9688/ |
@addaleax hmm wait, do we even need to put primitives into the memos? The memos are for cycles and is it even possible to make a cycle with primitives? (Or do we even care if they do since they are not compared recursively?) Also FWIW I think the example above should not throw because objects are not primitives and as the docs and the comments stare, for non-primitives we don't compare them by using operators. Objects with the same properties(or no properties), prototypes and tags should be considered equal. |
I don’t think so, for both questions – but I’m not sure it’s trivial to undo that without changing the logic here for really weird edge cases. |
Use a Map instead of an array for checking previously found cyclic references. This reduces complexity for an array-of-objects case from O(n²) to O(n·log n). Fixes: nodejs#12842 PR-URL: nodejs#12849 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
32c658d
to
bc62437
Compare
Landed in 7e5f500 |
Use a Map instead of an array for checking previously found cyclic references. This reduces complexity for an array-of-objects case from O(n²) to O(n·log n). Fixes: #12842 PR-URL: #12849 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
Use a Map instead of an array for checking previously found cyclic references. This reduces complexity for an array-of-objects case from O(n²) to O(n·log n). Fixes: nodejs#12842 PR-URL: nodejs#12849 Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
Use a Map instead of an array for checking previously found
cyclic references.
This reduces complexity for an array-of-objects case from
O(n²) to O(n·log n).
Case from the issue, new output:
Fixes: #12842
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)