Skip to content

Conversation

@TimothyGu
Copy link
Member

@TimothyGuTimothyGu commented Mar 19, 2017

Since v8/v8@532c16e (included in v5.6), using Object.create(null) directly is now faster than using a constructor.

Refs: emberjs/ember.js#15001
Refs: https://crrev.com/532c16eca071df3ec8eed394dcebb932ef584ee6

/cc @nodejs/v8

querystring
 improvement confidence p.value querystring/querystring-parse.js n=100000 type="altspaces" 5.40 % * 1.400406e-02 querystring/querystring-parse.js n=100000 type="encodefake" 5.70 % 6.377693e-02 querystring/querystring-parse.js n=100000 type="encodelast" 3.49 % 1.214386e-01 querystring/querystring-parse.js n=100000 type="encodemany" 5.08 % * 1.427018e-02 querystring/querystring-parse.js n=100000 type="manyblankpairs" -7.21 % ** 1.152391e-03 querystring/querystring-parse.js n=100000 type="manypairs" 8.53 % *** 6.779383e-10 querystring/querystring-parse.js n=100000 type="multicharsep" 6.11 % ** 8.321237e-03 querystring/querystring-parse.js n=100000 type="multivalue" 12.44 % ** 1.668042e-03 querystring/querystring-parse.js n=100000 type="multivaluemany" 19.43 % *** 1.919249e-09 querystring/querystring-parse.js n=100000 type="noencode" 6.23 % *** 2.348627e-04
map-bench
es/map-bench.jsmillions=3method="object": 0.39794620430879774 es/map-bench.jsmillions=3method="nullProtoObject": 0.41138972726767054 es/map-bench.jsmillions=3method="nullProtoLiteralObject": 0.37403618701313923 es/map-bench.jsmillions=3method="storageObject": 0.39906914551802625 es/map-bench.jsmillions=3method="fakeMap": 0.3576056060343219 es/map-bench.jsmillions=3method="map": 0.5858516350899148
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

lib

@TimothyGuTimothyGu added the performance Issues and PRs related to the performance of Node.js. label Mar 19, 2017
@nodejs-github-botnodejs-github-bot added dont-land-on-v4.x lib / src Issues and PRs related to general changes in the lib or src directory. whatwg-url Issues and PRs related to the WHATWG URL implementation. labels Mar 19, 2017
Copy link
Contributor

@cjihrigcjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM. I worry that this will make debugging more difficult though.

@joyeecheung
Copy link
Member

There seems to be some regressions in the event benchmarks...

See numbers
 improvement confidence p.value events/ee-add-remove.js n=250000 -43.29 % *** 4.520847e-30 events/ee-emit-multi-args.js n=2000000 1.57 % 2.325176e-01 events/ee-emit.js n=2000000 8.37 % ** 6.855389e-03 events/ee-listener-count-on-prototype.js n=50000000 -96.06 % *** 8.635026e-26 events/ee-listeners-many.js n=5000000 -17.51 % *** 3.849835e-27 events/ee-listeners.js n=5000000 -56.36 % *** 6.473337e-30 events/ee-once.js n=20000000 -51.46 % *** 4.847882e-33 

@TimothyGu
Copy link
MemberAuthor

Interesting.

The V8 optimization is mainly designed to accommodate map-like usage of objects, by automatically using so-called "slow" properties based on a hashmap instead of trying to find hidden classes. This makes it work especially well with cases like HTTP headers, FS cache, and query string parsing that don't have a fixed pattern of object properties.

On the other hand, an instance of EventEmitter often only has a few event names, and in case of the benchmarks they all only have one event 'dummy', so slow properties get (unfairly, arguably) penalized.

Take the benchmark with the worst regression, ee-listener-count-on-prototype.js. If we make the emitter have listeners for two events instead of just one, this PR actually drastically improves its performance:

 improvement confidence p.value events/ee-listener-count-on-prototype.js n=50000000 61.35 % *** 2.676747e-11 

I believe having two or more events is a considerably more common case than just having one event, and therefore I think this change is still justified.

benchmark diff
diff --git a/benchmark/events/ee-listener-count-on-prototype.js b/benchmark/events/ee-listener-count-on-prototype.js index dfe7e27..1ef0f09 100644 --- a/benchmark/events/ee-listener-count-on-prototype.js+++ b/benchmark/events/ee-listener-count-on-prototype.js@@ -9,12 +9,12 @@ function main(conf){var ee = new EventEmitter(); - for (var k = 0; k < 10; k += 1)- ee.on('dummy', function(){});+ for (var k = 0; k < 2 * 10; k += 1)+ ee.on(`dummy${k % 2}`, function(){}); bench.start(); for (var i = 0; i < n; i += 1){- ee.listenerCount('dummy');+ ee.listenerCount(`dummy${i % 2}`); } bench.end(n)}

@TimothyGu
Copy link
MemberAuthor

@cjihrig

I worry that this will make debugging more difficult though.

Can you elaborate? I doubt having a generic name like StorageObject instead of nothing would improve debugging experience, and EventHandlers though descriptive is only used for a property named _events, which announces the intention of the object well enough.

@cjihrig
Copy link
Contributor

I meant specifically in heap snapshots.

@joyeecheung
Copy link
Member

joyeecheung commented Mar 20, 2017

I think the benchmarks can add a few more configurations for different use cases.

Note that the diff in #11930 (comment) also removes the loop invariant(not sure if V8 could perform LICM on that previously).

Also +1 with @cjihrig, with Object.create(null) those objects probably won't be grouped together in heap snapshots and only have an id in their labels.

@mscdex
Copy link
Contributor

What's up with the 'manyblankpairs' regression?

@TimothyGu
Copy link
MemberAuthor

@mscdex, it can be attributed to the initial cost of object creation:

$ ./node benchmark/es/map-bench.js method=storageObjectCreation millions=4 es/map-bench.js millions=4 method="storageObjectCreation": 216.31376787921167 $ ./node benchmark/es/map-bench.js method=objectCreation millions=4 es/map-bench.js millions=4 method="objectCreation": 99.61981839655775 $ ./node benchmark/es/map-bench.js method=nullProtoObjectCreation millions=4 es/map-bench.js millions=4 method="nullProtoObjectCreation": 33.876842956365344 $ ./node --turbo benchmark/es/map-bench.js method=storageObjectCreation millions=4 es/map-bench.js millions=4 method="storageObjectCreation": 934.5947246800764 $ ./node --turbo benchmark/es/map-bench.js method=objectCreation millions=4 es/map-bench.js millions=4 method="objectCreation": 892.728215367111 $ ./node --turbo benchmark/es/map-bench.js method=nullProtoObjectCreation millions=4 es/map-bench.js millions=4 method="nullProtoObjectCreation": 28.154238858915456 

@joyeecheung
Copy link
Member

Before this PR (StorageObject have names and can be searched):

screen shot 2017-03-20 at 12 22 56 pm

After:

screen shot 2017-03-20 at 12 22 25 pm

I think the performance gain is probably worth it but wonder if @nodejs/v8 have any idea to work around this sacrafice

@mscdex
Copy link
Contributor

Is there any perf difference with StorageObject.prototype = null; ? I'm not sure why I didn't originally use that, but it seems to work similarly?

@lpinca
Copy link
Member

@mscdex

$ node > function StorageObject(){} undefined > StorageObject.prototype = null null > var o = new StorageObject() undefined > o. o.__defineGetter__ o.__defineSetter__ o.__lookupGetter__ o.__lookupSetter__ o.__proto__ o.constructor o.hasOwnProperty o.isPrototypeOf o.propertyIsEnumerable o.toLocaleString o.toString o.valueOf 

Copy link
Member

@jasnelljasnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes LGTM but needs a rebase

The object is used as a structure, not as a map, which `StorageObject` was designed for.
After V8 5.6, using Object.create(null) directly is now faster than using a constructor. Refs: emberjs/ember.js#15001 Refs: https://crrev.com/532c16eca071df3ec8eed394dcebb932ef584ee6
@TimothyGu
Copy link
MemberAuthor

@joyeecheung I added a commit that uses a dedicated class for url[context], which should make heap debugging for this case as easy as before. It also restores the performance loss caused by a switch to Object.create(null), since the context always has a fixed set of properties.

@TimothyGu
Copy link
MemberAuthor

Landed in d9b0e4c...cfc8422.

@TimothyGuTimothyGu deleted the storage-object branch March 24, 2017 22:26
TimothyGu added a commit to TimothyGu/node that referenced this pull request Mar 24, 2017
PR-URL: nodejs#11930 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: James M Snell <[email protected]>
TimothyGu added a commit to TimothyGu/node that referenced this pull request Mar 24, 2017
The object is used as a structure, not as a map, which `StorageObject` was designed for. PR-URL: nodejs#11930 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: James M Snell <[email protected]>
TimothyGu added a commit to TimothyGu/node that referenced this pull request Mar 24, 2017
After V8 5.6, using Object.create(null) directly is now faster than using a constructor for map-like objects. PR-URL: nodejs#11930 Refs: emberjs/ember.js#15001 Refs: https://crrev.com/532c16eca071df3ec8eed394dcebb932ef584ee6 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Luigi Pinca <[email protected]> Reviewed-By: James M Snell <[email protected]>
@mscdex
Copy link
Contributor

mscdex commented Mar 25, 2017

@TimothyGu I'm seeing contradicting results on master with some more simple, direct benchmarks:

'use strict';varn=1e7;varinput={mode: 0o666,flags: 'r',start: 5,end: 100};functionStorageObject(){}StorageObject.prototype=Object.create(null);functioncopyObject(source){consttarget=newStorageObject();for(varkeyinsource)target[key]=source[key];returntarget;}functioncopyObject2(source){consttarget=Object.create(null);for(varkeyinsource)target[key]=source[key];returntarget;}varr;console.time('copyObject');for(vari=0;i<n;++i){r=copyObject(input);r.mode>>>=0;}console.timeEnd('copyObject');console.time('copyObject2');for(vari=0;i<n;++i){r=copyObject2(input);r.mode>>>=0;}console.timeEnd('copyObject2');

Results:

copyObject: 2487.810ms copyObject2: 8500.589ms 

With just the first two properties in input:

copyObject: 1600.427ms copyObject2: 3985.290ms 

With just the first property in input:

copyObject: 838.353ms copyObject2: 2215.232ms 

@mscdex
Copy link
Contributor

I should add that I just tried with V8 5.7 which was just merged into master and the results are similar.

@mscdex
Copy link
Contributor

mscdex commented Mar 26, 2017

Ah, I just noticed that es/map-bench.js isn't actually benchmarking object creation, but instead property access. There is a much larger difference with object creation in StorageObject's favor compared to property access where StorageObject is still pretty close to the other top performing alternatives.

I think we should revert the changes in this PR (except maybe the URL changes where the properties are known beforehand?). /cc @nodejs/collaborators

@lpinca
Copy link
Member

lpinca commented Mar 26, 2017

I also noticed the huge difference with object creation time but this also "fixes" something weird which happens when using instances of

functionStorageObject(){}StorageObject.prototype=Object.create(null);

I've described the behavior in #11868 (comment). @mscdex can you take a look?

@twoi
Copy link

twoi commented Apr 1, 2019

This is a breaking change. Maybe it's faster now, but it is not backwards compatible, e.g. it broke the following code:

var reqArgs = url.parse(req.url, true).query; if (reqArgs.hasOwnProperty("id")) ... 

now throws TypeError: reqArgs.hasOwnProperty is not a function

@mcollina
Copy link
Member

@twoi I imagine you upgraded from a release to another, can you give us the version numbers? Was this a patch/minor release or a major release? Thanks.

@twoi
Copy link

twoi commented Apr 1, 2019

We upgraded from 0.8.15 to 11.9 - finally, because we are using a native module that (pre-napi) needed to ported / rebuilt on every Node.js version upgrade, which include all the vast v8 refactorings.

But this change apparently made it into Node.js version 8.0.0. It's filed under Semver-patch commits, even though I'd argue it's a breaking change.

@BridgeAR
Copy link
Member

@twoi this PR is semver-patch. The removal of the prototype was done in #6092 which is marked as semver-major (v6). Is there anything actionable you ask for?

I highly recommend to never use object.hasOwnProperty('foobar'). Instead just use:
Object.prototoype.hasOwnProperty.call(object, 'foobar'). That is a significantly safer way to do the same.

@twoi
Copy link

twoi commented Apr 2, 2019

@BridgeAR Thanks for the advice. Agree.

I don't have anything actionable, just wanted to make aware of the fact that this innocent looking change can (and in fact did) break someone's code.

Which, it seems, people noticed during testing: https://github.com/nodejs/node/pull/6092/files#r58880619
And then turned it semver-major.

I am adding more testing myself, so this won't catch me unawares anymore... Case closed.

@BridgeAR
Copy link
Member

@twoi

this innocent looking change can (and in fact did) break someone's code

The breaking change was done in #6092, not here. This PR is just some code refactoring.

@twoi
Copy link

twoi commented Apr 2, 2019

@BridgeAR I see. Makes sense. Thanks

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib / srcIssues and PRs related to general changes in the lib or src directory.performanceIssues and PRs related to the performance of Node.js.whatwg-urlIssues and PRs related to the WHATWG URL implementation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

11 participants

@TimothyGu@joyeecheung@cjihrig@mscdex@lpinca@twoi@mcollina@BridgeAR@jasnell@addaleax@nodejs-github-bot