-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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: optimize various functions #601
Conversation
// adding it to the listeners, first emit "newListener". | ||
if (events.newListener) | ||
this.emit('newListener', type, | ||
typeof listener.listener === 'function' ? |
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.
listener.listener
feels awkward, can the initial naming of this be tweaked at all?
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.
Maybe, I just left it as it was before. What do you suggest?
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.
@mscdex Can you put braces around the consequent?
General comment: There's a lot of |
if (!(events = this._events)) | ||
events = this._events = {}; | ||
else { | ||
existing = events[type]; |
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.
at this line events
is undefined
, isn't it?
events
variable can be initiated with
var events = this._events;
and here can be used like this:
if (!events)
events = this._events = {};
existing = events[type];
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.
No, it is defined there. I have the separate else there because of the code following this line (events.newListener
can't possibly exist if this._events
isn't set, so there's no need to check in that case).
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.
oh, I see, I was misled by the line nr. 137, was expecting of an equality check, actually using an assignment in an if statement is a bad practice. Would you mind to try the code that I proposed in my first comment and check the performance impact of the changes made?
I'm not sure what you mean by "significantly different?" The checks are still the same checks, except cached versions are used where possible (in addition to inline assignment). |
Benchmarks do look good. (+ Tests pass.) Before:
After:
|
@@ -47,7 +47,7 @@ EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { | |||
}; | |||
|
|||
function $getMaxListeners(that) { | |||
if (util.isUndefined(that._maxListeners)) | |||
if (that._maxListeners === void 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.
undefined
is not re-definable in ES5. Is there a good reason to use void 0
over it?
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.
It's what util.isUndefined()
was using.
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.
+0.1 for 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.
+1 for undefined
Mostly LGTM, but I'd like to see other potential comments. |
For me it also looks good, except the variable assignments inside if statements like @yaycmyk said, they are misleading while reading the code, the cached values should be assigned somewhere before the if statement as I proposed in this discussion |
return this; | ||
} | ||
|
||
// emit removeListener for all listeners on all events | ||
if (arguments.length === 0) { | ||
for (key in this._events) { | ||
var keys = Object.keys(events); |
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 suppose Object.keys() could have a negative impact on EventEmitter objects with many events because of the array it creates (instead of iterating over an object like for..in does.) Probably an uncommon case but it might be good to capture it in a benchmark.
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 suppose we could manually keep track of the length of this._events
and use some length value as the cut-off between using Object.keys()
and using a for-in?
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 is based on false assumption, V8 for-in eagerly creates an array of the properties before the loop body is even entered. In optimized case this array is cached but that's not the case for events
anyway.
So basically you never want to use a for-in, even in the case where you need prototype properties you are better off doing it manually with Object.keys
and Object.getPrototypeOf
(which could be turned into a function getInheritedKeys
to make it fast idiom everywhere)
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.
V8 for-in eagerly creates an array of the properties before the loop body is even entered.
That's only the case for "uncommon" objects, like object proxies or objects with interceptors. The objects that EventEmitter creates are simple objects with an enum cache that for..in iterates over.
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.
That's only the case for "uncommon" objects, like object proxies or objects with interceptors. The objects that EventEmitter creates are simple objects with an enum cache that for..in iterates over.
A normalized object is also an uncommon object which this._events
definitely is as there is a delete
call on it literally 18 lines below.
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.
so, maybe we should consider getting reed of delete
s ?
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.
@vkurchatkin That was tried in the past in the middle of v0.10 (unfortunately) which caused memory leaks (especially if you keep using uniquely named events).
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.
That's not the point, this._events
is being treated like a hash table - if it's not normalized by delete
it will be soon normalized by adding enough event names on it (16 the last time I checked was the limit on the heuristic). Just forget for-in forever and be happy, you know that Object.keys
uses the enum cache as well, right?
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.
you know that Object.keys uses the enum cache as well, right?
It still creates a new array every time though.
What you say about delete obj.key
and for..in is true for Crankshaft but not TurboFan. I'm not sure what we should be optimizing for. Crankshaft is still the default but it's effectively in maintenance mode. TurboFan is going to replace it some day but that may still be far off. Or it may be next month, I don't know. :-(
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.
Object normalization is not related to crankshaft and you cannot do a simple map check with normalized objects to see if a property was deleted during an iteration of the loop so I am sure for-in will always be very slow for normalized objects unless it's very simple body that can be analyzed to never delete properties but I am not holding my breath at all.
Some comments but I like the general thrust. |
a13ac82
to
f93f44a
Compare
Ok I have made suggested changes. I also went ahead and optimized some of the other functions. The changes to I benchmarked All tests still pass. EDIT: Ok, it seems now that for some reason if the array is large enough (~>=50 elements in my testing), |
f93f44a
to
d292fd2
Compare
Ok I made a tweak to the |
Hmm, the new changes are slightly slower on the ee-add-remove benchmark. Unpatched:
Previous patch:
Current patch:
|
@mscdex where are you getting your numbers from? it may be prudent to write additional benchmarks for these. :) |
@Fishrock123 I did write additional benchmarks to test the other functions, but was going to submit them in a separate PR. However I did not change ee-add-remove. The results for me are the same. Not a whole lot changed in the add/remove functions anyway, merely style changes. |
if (!this._events) | ||
this._events = {}; | ||
events = this._events; | ||
if (!events) |
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.
always include braces for conditionals please
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.
nope, one line body is ok without braces
d292fd2
to
05da7a7
Compare
I've also now made some improvements to Benchmark code: var Benchmark = require('benchmark');
var ee = new (require('events').EventEmitter)();
for (var k = 0; k < 10; k += 1)
ee.on('dummy', function() {});
var suite = new Benchmark.Suite();
suite.add('emitter#emit', function() {
// tested code
}, { minSamples: 100 });
suite.on('cycle', function(event) {
console.log(String(event.target));
});
suite.run({ async: false }); 0 args testee.emit('dummy'); Results:
1 arg testee.emit('dummy', 1); Results:
2 args testee.emit('dummy', 1, false); Results:
3 args testee.emit('dummy', 1, false, null); Results:
4 args test(this hits the slow case code) ee.emit('dummy', 1, false, null, 'foo'); Results:
|
If performance is a priority in iojs/io.js, I think @petkaantonov is the guy. He has done some amazing work by replacing node core stuff with faster (as in 100x) user land versions. |
@tjconcept There is already #643 for incorporating his url module. Anyway, I think these changes are a good start. |
@bnoordhuis rebased. |
@mscdex It seems to break parallel/test-event-emitter-add-listeners and message/stdin_messages:
|
That's strange ... I did a clean |
I landed PR #687 earlier today. It's possible that it's interacting badly with this PR. |
4107f84
to
faf4841
Compare
Ok, tests now pass again :-) |
@@ -55,22 +55,67 @@ EventEmitter.prototype.getMaxListeners = function getMaxListeners() { | |||
return $getMaxListeners(this); | |||
}; | |||
|
|||
function emitNone(handler, isFn, self) { |
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.
a comment above these might be nice.
ee-add-remove consistently sees an 80+% gain with this patch.
LGTM. @trevnorris Can you take a look since you were doing #533? (Side-note: we need some |
RE: additional benchmarks, here are some that I created during my testing FWIW. |
Mind PR-ing those soon? :) |
@Fishrock123 Done: #730. |
Cache events and listeners objects where possible and loop over Object.keys() instead of using for..in. These changes alone give ~60-65% improvement in the ee-add-remove benchmark. The changes to EventEmitter.listenerCount() gives ~14% improvement and changes to emitter.listeners() gives significant improvements for <50 listeners (~195% improvement for 10 listeners). The changes to emitter.emit() gives 3x speedup for the fast cases with multiple handlers and a minor speedup for the slow case with multiple handlers. The swapping out of the util.is* type checking functions with inline checks gives another ~5-10% improvement.
faf4841
to
bf8d55c
Compare
Haven't taken the time to run the tests myself, but the code LGTM. |
Some results of the new benchmarks added in 847b9d2 with the iterations in #746
(I frequently am getting a 5-10% regression on that ee-listeners-many.js benchmark) |
Yeah, I wasn't quite sure about the heuristics that I used for |
I'm willing to chalk up the events/ee-listeners-many regression as a fluke. The benchmark seems to do the same amount of work with and without this pull request applied: memory usage is about the same, output of @Fishrock123 @trevnorris Thoughts? |
LGTM, for the record. Can I get one more @iojs/collaborators LGTM? |
LGTM |
Cache events and listeners objects where possible and loop over Object.keys() instead of using for..in. These changes alone give ~60-65% improvement in the ee-add-remove benchmark. The changes to EventEmitter.listenerCount() gives ~14% improvement and changes to emitter.listeners() gives significant improvements for <50 listeners (~195% improvement for 10 listeners). The changes to emitter.emit() gives 3x speedup for the fast cases with multiple handlers and a minor speedup for the slow case with multiple handlers. The swapping out of the util.is* type checking functions with inline checks gives another ~5-10% improvement. PR-URL: #601 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Evan Lucas <evanlucas@me.com>
Thanks Brian, landed in b677b84. |
Cache events and listeners objects where possible and loop over
Object.keys() instead of using for..in. These changes alone give
~60-65% improvement in the ee-add-remove benchmark.
The swapping out of the util type checking functions with inline
checking gives another ~5-10% improvement.