Skip to content
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

module.exports = {} approach and objects with fast / slow properties #11430

Closed
vsemozhetbyt opened this issue Feb 16, 2017 · 8 comments
Closed
Labels
performance Issues and PRs related to the performance of Node.js.

Comments

@vsemozhetbyt
Copy link
Contributor

vsemozhetbyt commented Feb 16, 2017

  • 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

@indutny
Copy link
Member

indutny commented Feb 16, 2017

Considering that it takes nanoseconds to access them, I wonder if this improvement is visible at all.

@jasnell
Copy link
Member

jasnell commented 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 mscdex added the performance Issues and PRs related to the performance of Node.js. label Feb 16, 2017
@mscdex
Copy link
Contributor

mscdex commented Feb 16, 2017

I just wanted to add that using module.exports = .... instead of peppered exports.* = ... 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
Copy link
Contributor Author

@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 like const fs = require('fs');, the whole module.exports is imported. If it is a slow object and a user uses any fs.* property in a heavy loop, there could be a performance loss.

@vsemozhetbyt
Copy link
Contributor Author

vsemozhetbyt commented Feb 17, 2017

Hmm, it is strange. It seems all the imported core modules are fast objects:

require('repl')._builtinLibs.forEach((name) => {
  const mod = require(name);
  const keyNum = Reflect.ownKeys(mod).length;
  console.log(`${name}: ${%HasFastProperties(mod) ? 'fast' : 'slow'} (${keyNum} keys)`);
});
assert: fast (17 keys)
buffer: fast (5 keys)
child_process: fast (9 keys)
cluster: fast (15 keys)
crypto: fast (44 keys)
dgram: fast (3 keys)
dns: fast (42 keys)
domain: fast (5 keys)
events: fast (8 keys)
fs: fast (81 keys)
http: fast (13 keys)
https: fast (6 keys)
net: fast (12 keys)
os: fast (19 keys)
path: fast (15 keys)
punycode: fast (6 keys)
querystring: fast (7 keys)
readline: fast (7 keys)
repl: fast (8 keys)
stream: fast (10 keys)
string_decoder: fast (1 keys)
tls: fast (17 keys)
tty: fast (3 keys)
url: fast (9 keys)
util: fast (28 keys)
v8: fast (3 keys)
vm: fast (8 keys)
zlib: fast (74 keys)

Maybe v8 somehow optimizes them before/after importing or in another time.

@joyeecheung
Copy link
Member

@vsemozhetbyt
Copy link
Contributor Author

vsemozhetbyt commented Feb 17, 2017

@joyeecheung Oh, I see. Thank you. It seems the limit of fast non-keyed incrementation is 133 keys:

Test (click me):
const obj = {};
obj.x001 = 0;
obj.x002 = 0;
obj.x003 = 0;
obj.x004 = 0;
obj.x005 = 0;
obj.x006 = 0;
obj.x007 = 0;
obj.x008 = 0;
obj.x009 = 0;
obj.x010 = 0;
obj.x011 = 0;
obj.x012 = 0;
obj.x013 = 0;
obj.x014 = 0;
obj.x015 = 0;
obj.x016 = 0;
obj.x017 = 0;
obj.x018 = 0;
obj.x019 = 0;
obj.x020 = 0;
obj.x021 = 0;
obj.x022 = 0;
obj.x023 = 0;
obj.x024 = 0;
obj.x025 = 0;
obj.x026 = 0;
obj.x027 = 0;
obj.x028 = 0;
obj.x029 = 0;
obj.x030 = 0;
obj.x031 = 0;
obj.x032 = 0;
obj.x033 = 0;
obj.x034 = 0;
obj.x035 = 0;
obj.x036 = 0;
obj.x037 = 0;
obj.x038 = 0;
obj.x039 = 0;
obj.x040 = 0;
obj.x041 = 0;
obj.x042 = 0;
obj.x043 = 0;
obj.x044 = 0;
obj.x045 = 0;
obj.x046 = 0;
obj.x047 = 0;
obj.x048 = 0;
obj.x049 = 0;
obj.x050 = 0;
obj.x051 = 0;
obj.x052 = 0;
obj.x053 = 0;
obj.x054 = 0;
obj.x055 = 0;
obj.x056 = 0;
obj.x057 = 0;
obj.x058 = 0;
obj.x059 = 0;
obj.x060 = 0;
obj.x061 = 0;
obj.x062 = 0;
obj.x063 = 0;
obj.x064 = 0;
obj.x065 = 0;
obj.x066 = 0;
obj.x067 = 0;
obj.x068 = 0;
obj.x069 = 0;
obj.x070 = 0;
obj.x071 = 0;
obj.x072 = 0;
obj.x073 = 0;
obj.x074 = 0;
obj.x075 = 0;
obj.x076 = 0;
obj.x077 = 0;
obj.x078 = 0;
obj.x079 = 0;
obj.x080 = 0;
obj.x081 = 0;
obj.x082 = 0;
obj.x083 = 0;
obj.x084 = 0;
obj.x085 = 0;
obj.x086 = 0;
obj.x087 = 0;
obj.x088 = 0;
obj.x089 = 0;
obj.x090 = 0;
obj.x091 = 0;
obj.x092 = 0;
obj.x093 = 0;
obj.x094 = 0;
obj.x095 = 0;
obj.x096 = 0;
obj.x097 = 0;
obj.x098 = 0;
obj.x099 = 0;
obj.x100 = 0;
obj.x101 = 0;
obj.x102 = 0;
obj.x103 = 0;
obj.x104 = 0;
obj.x105 = 0;
obj.x106 = 0;
obj.x107 = 0;
obj.x108 = 0;
obj.x109 = 0;
obj.x110 = 0;
obj.x111 = 0;
obj.x112 = 0;
obj.x113 = 0;
obj.x114 = 0;
obj.x115 = 0;
obj.x116 = 0;
obj.x117 = 0;
obj.x118 = 0;
obj.x119 = 0;
obj.x120 = 0;
obj.x121 = 0;
obj.x122 = 0;
obj.x123 = 0;
obj.x124 = 0;
obj.x125 = 0;
obj.x126 = 0;
obj.x127 = 0;
obj.x128 = 0;
obj.x129 = 0;
obj.x130 = 0;
obj.x131 = 0;
obj.x132 = 0;
obj.x133 = 0;

console.log(%HasFastProperties(obj));

obj.x134 = 0;

console.log(%HasFastProperties(obj));
true
false

But actually this is another limit here: limit of the overall number of keys:

var o1 = {};
var o2 = {};

for (var i = 1; %HasFastProperties(o2); i++) {
  o1['p' + i] = 1;
  o2 = JSON.parse(JSON.stringify(o1));
}

console.log(Object.keys(o2).length);
134

@vsemozhetbyt
Copy link
Contributor Author

Sorry, I will close it as not relevant for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Issues and PRs related to the performance of Node.js.
Projects
None yet
Development

No branches or pull requests

5 participants