-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
lib: faster event listening #41309
lib: faster event listening #41309
Conversation
The current EventEmitter uses arrays to control multiple listeners, but that means that prependListener and removeListeners operations have an O(N) time complexity. With this change, instead of an array we use a map + a linked list (to preserve execution order), which gives an O(1) time complexity for those operations. This will be especially helpful in scenarios where we have many listeners to the same event. BREAKING CHANGE: I don't know how much this is a breaking change, because if someone is using the _events property, I think it is bad usage that has no guarantees to keep working. Regardless, the _events property will no longer be a function or an array, but a function or a "MappedLinkedList". Some of the operations from the array are present, though, like [Symbol.iterator], unshift and push, making it possible to adapt some code that is arbritary using it
Review requested:
|
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.
Thanks, this really looks good! You might want to add a few benchmarks or run existing ones in case they already indicate improvements and post the results here.
About the breaking change: I do not think we are able to break the property usage. We could however add a private/symbol property, use that internally and use a proxy on the _events
property that maps the entries to prevent breaking existing usage.
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.
Linked lists are almost never ever better than arrays because often code is dominated by memory access and cache locality is a lot bigger of a deal than doing O(n)
operations.
Additionally, prependListener
is not the most common use case to optimize for.
I am open to being wrong - this definitely needs a benchmark :)
(Aside, good job figuring out all the bits of working with the code for your first contribution!)
Thank you, guys. I'll work on the benchmarks after Christmas and post them here. I think it's expected that a better performance is achieved when we have many listeners at the same event. @benjamingr in this particular case of use, this linked list is only there to preserve execution order, what will really make a difference in performance is the Map, I think. But I'm not sure if this change can bring a better performance overall, there's really a memory trade-off here and in most cases the average program doesn't add too many listeners to the same event, so maybe this change would bring benefits only in edge cases. But I'll benchmark this ASAP |
@Farenheith that sounds very reasonable to me. Feel free to ping me if you have any issue running the benchmarks. I agree it's probably a trade-off. These PRs are good by the way, even if the code doesn't eventually get merged they are a great idea to build an intuition for the code and figure out what's slow/fast/needs work :) (As a side note, we all happen to be guys in this conversation but this project has a lot of non male (and non binary) contributors that make up a significant portion of the project, the code written and the leadership - so it is best to avoid "guys" and gendered pronouns in PRs for "folks" "people" etc) |
@benjamingr thank you for the tip about "guys". I actually thought it was a gender-neutral term because of some literal translation from my mother language lol. But it's good to know better about it. |
Folks, I want to ask about the file I added: mapped_linkedlist. |
@BridgeAR that's a really good idea. I'll work on that |
Something else to consider here is that while the |
To make sure the removing of method follows a LIFO logic when the same listener is added twice, it's important to do a pop instead of a shift operation
@jasnell @BridgeAR @benjamingr I ran the benchmarks and the results aren't good.
|
@Farenheith this has a loooooong history of being attempted, without success, see #21856 and #17074 for prior takes. |
@apapirovski I was thinking about it and I suppose the catch here is that not only prepend is a very uncommon operation, but also the removeListener operation tends to remove the last listener of the array in most of the uses, so there's not much room for optimization. |
The current EventEmitter uses arrays to control multiple listeners,
but that means that prependListener and removeListeners operations
have an O(N) time complexity. With this change, instead of an array
we use a map + a linked list (to preserve execution order), which
gives an O(1) time complexity for those operations. This will be
especially helpful in scenarios where we have many listeners to
the same event.
BREAKING CHANGE: I don't know how much this is a breaking change,
because if someone is using the _events property, I think it is bad
usage that has no guarantees to keep working. Regardless, the _events
property will no longer be a function or an array, but a function
or a "MappedLinkedList". Some of the operations from the array are
present, though, like [Symbol.iterator], unshift and push, making
it possible to adapt some code that is arbritary using it