Repository navigation
module.exports = {} approach and objects with fast / slow properties #11430
Description
- Version: maybe all
- Platform: maybe all
- Subsystem: module
There were some PRs recently that refactor module.exports filling from incremental to atomic.
There also were some concerns about the gain of this approach.
It came to pass that I've recently read some stuff about v8 optimization concerning objects. There is a distinction between objects with fast and slow properties, and one way to make an object slow is exactly incremental property definition.
So I've written a simple test to define how many added properties make an object slow:
for (var i = 1, o = {}; %HasFastProperties(o); i++) o['p' + i] = 1;
console.log(Object.keys(o).length);Then I've run this code with some Node.js versions:
node --allow-natives-syntax test.js
// till at least v8 4.5.103.43 (Node.js 4.7.2)
16
// from v8 4.5.103.43 (Node.js 4.7.2) and so on
20
You could also check this code in the some last Node.js:
const slowObj = {};
for (let i = 1; i <= 20; i++) slowObj[`p${i}`] = 1;
const fastObj = JSON.parse(JSON.stringify(slowObj));
console.log(%HasFastProperties(slowObj), %HasFastProperties(fastObj));false true
This is also a double benchmark, with benchmark.js and a naive approach:
/******************************************************************************/
'use strict';
/******************************************************************************/
const slowObj = {};
for (let i = 1; i <= 20; i++) slowObj[`p${i}`] = 1;
const fastObj = JSON.parse(JSON.stringify(slowObj));
const testCases = [
function slow() { return slowObj.p20; },
function fast() { return fastObj.p20; },
];
/******************************************************************************/
console.log(`
1. Balanced benchmark
`);
/******************************************************************************/
const suite = require('benchmark').Suite();
testCases.forEach((testCase) => { suite.add(testCase.name, testCase); });
suite.on('cycle', (evt) => { console.log(String(evt.target)); }).run({ async: false });
/******************************************************************************/
console.log(`
2. Simplified benchmark
`);
/******************************************************************************/
const warmup = () => {};
[warmup, ...testCases].forEach((testCase) => {
const MS_IN_S = 1000;
const numberOfCycles = 1e8;
const start = Date.now();
for (let i = 0; i < numberOfCycles; i++) testCase();
if (testCase.name !== 'warmup') {
console.log(`${testCase.name} x ${
Math.round(numberOfCycles / ((Date.now() - start) / MS_IN_S)).toLocaleString()
} ops/sec`);
}
}); 1. Balanced benchmark
slow x 36,283,960 ops/sec ±1.32% (86 runs sampled)
fast x 46,327,002 ops/sec ±1.39% (85 runs sampled)
2. Simplified benchmark
slow x 38,284,839 ops/sec
fast x 61,881,188 ops/sec
I am not completely sure this should be considered as a reason for the mentioned refactoring, but I feel somehow obliged to share this data.
cc @nodejs/v8
Activity
indutny commentedon Feb 16, 2017
Considering that it takes nanoseconds to access them, I wonder if this improvement is visible at all.
jasnell commentedon Feb 16, 2017
@indutny ... in the benchmarks that I have run, there has been an average improvement of about 5% when switching, particularly on modules that are frequently used.
mscdex commentedon Feb 16, 2017
I just wanted to add that using
module.exports = ....instead of pepperedexports.* = ...statements is not only about performance but about organization (a nice side effect). It's much easier to see what is being exported when it's all in one place.vsemozhetbyt commentedon Feb 17, 2017
@indutny If I get it right, it takes nanoseconds if a user imports a separate property or a method, like
const read = require('fs').read;. However, if a user does likeconst fs = require('fs');, the wholemodule.exportsis imported. If it is a slow object and a user uses anyfs.*property in a heavy loop, there could be a performance loss.vsemozhetbyt commentedon Feb 17, 2017
Hmm, it is strange. It seems all the imported core modules are fast objects:
Maybe v8 somehow optimizes them before/after importing or in another time.
joyeecheung commentedon Feb 17, 2017
@vsemozhetbyt You might be interested in a look at https://github.com/nodejs/node/blob/master/deps/v8/test/mjsunit/fast-non-keyed.js and https://github.com/nodejs/node/blob/master/deps/v8/test/mjsunit/es6/symbols.js
vsemozhetbyt commentedon Feb 17, 2017
@joyeecheung Oh, I see. Thank you. It seems the limit of fast non-keyed incrementation is 133 keys:
Test (click me):
But actually this is another limit here: limit of the overall number of keys:
vsemozhetbyt commentedon Feb 17, 2017
Sorry, I will close it as not relevant for this case.