Sitelet https://github.com/nodejs/node/issues/11430
Skip to content

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

Description

@vsemozhetbyt
  • 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

  1. indutny commented on Feb 16, 2017

    @indutny
    Member

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

  2. jasnell commented on Feb 16, 2017

    @jasnell
    Member

    @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.

  3. added
    performanceIssues and PRs related to the performance of Node.js.
    on Feb 16, 2017
  4. mscdex commented on Feb 16, 2017

    @mscdex
    Contributor

    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.

  5. vsemozhetbyt commented on Feb 17, 2017

    @vsemozhetbyt
    ContributorAuthor

    @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.

  6. vsemozhetbyt commented on Feb 17, 2017

    @vsemozhetbyt
    ContributorAuthor

    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.

  7. joyeecheung commented on Feb 17, 2017

    @joyeecheung
    Member
  8. vsemozhetbyt commented on Feb 17, 2017

    @vsemozhetbyt
    ContributorAuthor

    @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
    
  9. vsemozhetbyt commented on Feb 17, 2017

    @vsemozhetbyt
    ContributorAuthor

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceIssues and PRs related to the performance of Node.js.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions