Uh oh!
There was an error while loading. Please reload this page.
events: speed up emit by create faster null prototype object - #39939
events: speed up emit by create faster null prototype object#39939wwwzbwcom wants to merge 1 commit into
Conversation
targos
commented
Aug 30, 2021
@nodejs/v8 is there an explanation to this large difference in speed? |
lpinca
commented
Aug 30, 2021
I think |
lpinca
commented
Aug 30, 2021
See also #11930. |
targos
commented
Aug 30, 2021
The problem with this micro-benchmark is that it tests with only one event name. This is not representative of real use of event emitters. |
targos
commented
Aug 30, 2021
We need at least to compare the other |
Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1047/ Results |
wwwzbwcom
commented
Aug 30, 2021
@targos here is the full benchmark result, |
benjamingr
left a comment
There was a problem hiding this comment.
This doesn't seem right, I'd assume that Object.create(null) literally calls NewJSObjectWithNullProto which in turn just does NewJSObject (similar to your = {} here) and then SetPrototype.
I'd like to understand this change better first.
benjamingr
commented
Aug 30, 2021
Looking at the code from what I understand - I'm more confused than when I started :) |
wwwzbwcom
commented
Aug 30, 2021
@benjamingr Yes it's strange, here is a simple test: |
wwwzbwcom
commented
Aug 30, 2021
@benjamingr I check the source code, looks like |
targos
commented
Aug 30, 2021
Yeah, but that's the problem of this benchmark. Fast properties work because the object always has the same shape. In the real world, many event emitters are created, with different events, and this is no longer true. |
wwwzbwcom
commented
Aug 30, 2021
@targos I dont think so, different ee can have different event, but as some event such store as fast property, we can take benefit when getting its handler |
wwwzbwcom
commented
Aug 30, 2021
Also most ee has similar shape such as stream all have data and close events |
I don't understand why this would get fast properties in one way or not another - there are several heuristics for fast properties - being a prototype is one but I don't understand why it'd happen here in one case and not the other. (And if it does, I doubt it's a good optimization). To be clear: I am thankful for your contribution and am just confused about the behaviour we're seeing. Can you run the code with v8 native syntax turned on (Or run with |
Oh and to illustrate the point take the code here:
And see what happens if you invert the order of the test :) (check .create first) |
This is definitely a breaking change as there is code in the ecosystem that (rightly or wrongly) accesses |
wwwzbwcom
commented
Aug 30, 2021
@jasnell Yes, the prototype is alwasy Map is faster than |
wwwzbwcom
commented
Aug 30, 2021
@benjamingr Welcome to Node.js v14.17.3.
Type ".help"for more information.
> require('v8').setFlagsFromString('--allow-natives-syntax')
undefined
> %HasFastProperties(Object.create(null))
false> %HasFastProperties({})
true>let a = {};
undefined
> Object.setPrototypeOf(a, null);
[Object: null prototype] {}
> %HasFastProperties(a)
true |
wwwzbwcom
commented
Aug 30, 2021
Yes, when you switch the order, the result is different: constnow=()=>{consthrTime=process.hrtime();returnhrTime[0]*1000+hrTime[1]/1000000;};leta,start,end;for(leti=0;i<10;i++){a=Object.create(null);start=now();for(leti=0;i<1e8;i++){a['data'];}end=now();console.log(`Object.create`,end-start);a={};Object.setPrototypeOf(a,null);start=now();for(leti=0;i<1e8;i++){a['data'];}end=now();console.log(`Object.setPrototypeOf`,end-start);}/*Object.create 775.9817728996277Object.setPrototypeOf 488.72393321990967Object.create 909.9279327392578Object.setPrototypeOf 481.3809299468994Object.create 895.326687335968Object.setPrototypeOf 473.78826904296875Object.create 899.6548957824707Object.setPrototypeOf 474.5387659072876Object.create 898.921669960022Object.setPrototypeOf 477.34186267852783Object.create 895.6246657371521Object.setPrototypeOf 473.7106132507324Object.create 894.2418832778931Object.setPrototypeOf 473.29633617401123Object.create 908.3455362319946Object.setPrototypeOf 472.2995858192444Object.create 895.0272059440613Object.setPrototypeOf 474.7768030166626Object.create 894.8622059822083Object.setPrototypeOf 474.1221857070923*/ |
devsnek
commented
Aug 30, 2021
I would be concerned that in real-world usage the backing object here will deoptimize to dictionary mode anyway. I'd appreciate seeing some more representative benchmarks. |
benjamingr
commented
Aug 30, 2021
@devsnek I think an argument @wwwzbwcom is making here is that in real-world usage many event emitters only emit 1-2 event types. I see different results for %HasFastProperties after emitting 100 events on each (both are fast properties :)) |
wwwzbwcom
commented
Aug 31, 2021
Agree that, some realworld usage benchmark is need. |
wwwzbwcom
commented
Aug 31, 2021
@devsnek slightly improvement in readable-from, more realworld benchmark later |
benjamingr
commented
Aug 31, 2021
I'll explain why I'm suspicious. 17.8% is a big improvement in |
camillobruni
commented
Aug 31, 2021
Late to the party. You're observation is correct, both This is based on the assumptions that folks use null-proto objects as dictionaries, in which case the fast-mode structure-tracking in V8 causes more harm than good. Especially all secondary effects with ICs are not represented in your micro benchmark, which is also one of the main motivations for "exotic" behavior in V8. The reasoning behind this, is that if you have known property-names you're likely fine with non-null prototype, since you don't accidentally read those. However, with unknown property names (and that's the dictionary/Map use-case), you have to use a null-proto in order to get correct behavior. Thus the null-proto is quite a good indication for this behavior. Let me know if you I can help with additional information. |
benjamingr
commented
Aug 31, 2021
@camillobruni thanks that's educational! and setting |
camillobruni
commented
Aug 31, 2021
I think that's quite an uncommon case that's not widely adopted for this pattern. Would a custom non-null prototype als be a solution for you? |
benjamingr
commented
Aug 31, 2021
I think it makes sense to default to shapes/hidden-classes here since it'll fall back to "slow mode" anyway if you add many handlers. Note that event lists aren't really dictionaries since often you sign up to multiple events once and then you dispatch events many times. That said, I have no idea why the |
wwwzbwcom
commented
Aug 31, 2021
in this benchmark, it does not just read from the stream, but create a handler on the stream "data" event, so as the ee is faster, this benchmark change 17% |
benjamingr
commented
Sep 1, 2021
ronag
commented
Sep 1, 2021
I agree with this: #39939 (comment) |
benjamingr
commented
Sep 1, 2021
@ronag I meant - what do you think of this change (optimizing for subscribing to fewer static events treating event lists in emitters as more static than not - from the streams PoV) |
From stream point of view I think it's a good idea. Streams usually don't add/remove events and there is a limited number of events (data/drain/readable, end/finish, error, close). I think we've tried avoid dynamic patterns. So usually there is 4 static events + handlers per stream. |
benjamingr
commented
Sep 1, 2021
Ok, I think I'm fine with this :) It:
Also ping @mscdex |
BridgeAR
commented
Sep 13, 2025
I am going to close this due to the issues discussed and since we didn't get a conclusion until now. Please open a new PR, if you believe we should still do this. |
We need to create null prototype to prevent internal method like
toStringmess with event handling.But in current version v8, Object create by
ObjectSetPrototypeOfis much more efficient thanObjectCreate(null)Here is benchmark result: