-
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
events: remove emit micro-optimizations #16869
Conversation
With improvements in V8, using separate emit functions is no longer necessary and can instead be replaced by the spread operator. improvement confidence p.value events/ee-emit.js n=2000000 2.98 % 0.09852489 events/ee-emit-2-args.js n=2000000 4.19 % *** 0.0001914216 events/ee-emit-6-args.js n=2000000 61.69 % *** 6.611964e-35 events/ee-emit-diff-args.js n=2000000 -0.36 % 0.305069 events/ee-once.js n=20000000 6.42 % *** 1.27831e-06
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.
nice! 👏
Benchmark CI: https://ci.nodejs.org/job/benchmark-node-micro-benchmarks/23/ Results
|
We should just have one ee-emit.js benchmark which has a parameter for number of args, something like 0-6 maybe. |
Also, we should probably make the number of listeners configurable as well, so that we can make sure there are no regressions for smaller numbers of listeners (e.g. 2-9). |
Is the AIX failure related? Anyone know? Doesn't seem like it but... |
It is failing since yesterday, not related. |
benchmark/events/ee-emit-4-args.js
Outdated
|
||
const ee = new EventEmitter(); | ||
|
||
for (var k = 0; k < 10; k += 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.
Coding style question: why not let
? (Not just here but throughout the patch)
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.
its much slower because it creates a closure for the variable in each loop iteration
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.
@mathiasbynens We have a lint rule re: this so that's the main reason. I think @TimothyGu tried to change it recently and feedback from @bmeurer was that we shouldn't quite yet.
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.
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.
Here's the relevant PR with more conversation: #15648
handler.apply(this, args); | ||
} else { | ||
const len = handler.length; | ||
const listeners = arrayClone(handler, len); |
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.
Can you leave a TODO here to consider switching to Array.prototype.slice
once V8 6.4 lands in Node? Or even to evaluate the idea of avoiding the defensive copy on emit
and rather making sure that the handler
itself is never mutated?
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 modulo comment.
Seeing this happening makes me happy!
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.
Making my concerns more explicit ...
We should just have one ee-emit.js benchmark which has a parameter for number of args, something like 0-6 maybe.
Also, we should probably make the number of listeners configurable as well, so that we can make sure there are no regressions for smaller numbers of listeners (e.g. 2-9).
New benchmark CI for the changes: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/28/
I have absolutely no clue why there's exactly one benchmark that tanked on the new version:
As far as I can tell this is almost equivalent to the old |
Ok, that benchmark needs to be tweaked. There's an optimization happening when using |
Ok, created a second version of the benchmark that doesn't use Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/31/ (The reason I changed it is that using |
210602d
to
7ddf389
Compare
New benchmark results:
|
@mscdex PTAL |
@mscdex or anyone else that reviewed this, PTAL. The "changes requested" needs to be dismissed before we can land this. |
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.
Still LGTM
We can make things faster for multiple listeners by adding some fast paths in function arrayClone(arr, n) {
switch (n) {
case 0: return [];
case 1: return [arr[0]];
case 2: return [arr[0], arr[1]];
case 3: return [arr[0], arr[1], arr[2]];
case 4: return [arr[0], arr[1], arr[2], arr[3]];
case 5: return [arr[0], arr[1], arr[2], arr[3], arr[4]];
}
// Not included in 'default' case because of perf issue with `const` in
// a switch case
const copy = new Array(n);
copy[0] = arr[0];
copy[1] = arr[1];
copy[2] = arr[2];
copy[3] = arr[3];
copy[4] = arr[4];
copy[5] = arr[5];
for (var i = 6; i < n; ++i)
copy[i] = arr[i];
return copy;
} I chose 5 as the max array length for fast paths as that seems like a reasonable limit to me. Also, 0 and 1-length arrays are supported for backwards compatibility, otherwise we could remove those cases since those kinds of arrays will not be generated internally (I'm thinking about modules that may directly mutate arrays in |
@mscdex Isn't that a bit outside the scope of this PR specifically, considering this PR does not touch that piece of code? |
@TimothyGu No? To me this PR is about improving |
@mscdex I guess @apapirovski can say for sure. Personally, I thought it was about making the code more maintainable/understandable by removing CrankshaftScript optimizations that no longer provide significant benefit. The fact that it also improves (rather than merely preserves) performance may have been incidental. I also prefer PRs to remain narrowly scoped on principle. If we put the |
From what I've seen, benchmarks are often not checked when backporting, which is especially important when going from TurboFan to Crankshaft. Whatever though, I just thought I'd throw the perf improvement out there for anyone interested. |
I see no reason not to add @mscdex's suggestion as a second separate commit in this PR. |
@mscdex These |
@bmeurer I'm not sure what you're asking, but we've always made a copy up front in case the event handlers change during the execution of event handlers. |
Right, but wouldn't it be possible to treat the handlers array as copy on write? Such that when iterating over them, you sort of take ownership of the handlers array and in case someone adds/removes handlers, you create a new Array instead of mutating the existing one. |
@bmeurer How would do you get notified when the array changes? |
I could be wrong, but it sure looks like we may be spiraling off into a conversation about whether and how to implement the |
By the way, the only thing preventing this from landing at this point is the objection from @mscdex. So I guess the question is whether the objection has been effectively cleared by the benchmark fixes/changes that @apapirovski did in response? Or is there still an objection to landing this without the |
Sorry, I haven't had time the past couple of days to address much of what's been discussed here. But as mentioned, the intent was mostly to remove "ugly" code that wasn't improving performance any longer. The speed up for emits with many arguments is a just a nice boon. (Further evidenced by the quote in the original: "With improvements in V8, using separate emit functions is no longer necessary. ") Re: the ensuing discussion, I think @bmeurer has a valid point re: allocating new handler array when an event is attached as opposed to emitted. That's something that could be benchmarked and tested. I might look at it if I have time this week. In the meantime, I'll be landing this shortly. |
Landed in f44f18a |
With improvements in V8, using separate emit functions is no longer necessary and can instead be replaced by the spread operator. improvement confidence p.value events/ee-emit.js n=2000000 2.98 % 0.09852489 events/ee-emit-2-args.js n=2000000 4.19 % *** 0.0001914216 events/ee-emit-6-args.js n=2000000 61.69 % *** 6.611964e-35 events/ee-emit-diff-args.js n=2000000 -0.36 % 0.305069 events/ee-once.js n=20000000 6.42 % *** 1.27831e-06 PR-URL: #16869 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Evan Lucas <[email protected]> Reviewed-By: Bryan English <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: Benedikt Meurer <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Brian White <[email protected]>
With improvements in V8, using separate emit functions is no longer necessary and can instead be replaced by the spread operator. improvement confidence p.value events/ee-emit.js n=2000000 2.98 % 0.09852489 events/ee-emit-2-args.js n=2000000 4.19 % *** 0.0001914216 events/ee-emit-6-args.js n=2000000 61.69 % *** 6.611964e-35 events/ee-emit-diff-args.js n=2000000 -0.36 % 0.305069 events/ee-once.js n=20000000 6.42 % *** 1.27831e-06 PR-URL: #16869 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Evan Lucas <[email protected]> Reviewed-By: Bryan English <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: Benedikt Meurer <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Brian White <[email protected]>
With improvements in V8, using separate emit functions is no longer necessary. This is not applicable to v8.x or earlier (the earlier version that's baking is though).
ee-emit-diff-args.js
was a custom benchmark to confirm whether there was any deoptimization happening when the function pattern — number of arguments passed in — would change unpredictably (between 0 - 3 arguments, so what used to be the fast path). I also had a version of that benchmark with 0-4 arguments and that one was +20% or so, due to the improvements inemit
performance for more than 3 arguments.Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
benchmark, events