Javascript: Module or module

Javascript: Module or module

When you have used Javascript for a while you have probably made a habbit on how to write your modules. I know I have. I've made my own boilerplate, so it's easy to start building a new module.

But the big question is: Is my module boilerplate fast enough?

You can run the test yourself and automagically post the results to my API :)

https://github.com/5orenso/javascript-module-performance

Note! All tests are run using Node.js version 0.10.33, 0.11.14 and 0.12.0.

Preparing the run script

Everything is kept as plain as possible to try to only measure the raw performance of module function calls. I'm running one type of loop each time to force the V8 optimizer to only look at this setup and start fresh every time.

Version 1

'use strict';
var app_path = __dirname + '/../';

var util = function (options) {
    var opts = options || {};
    return {
        yo: function (message) {
            return message;
        }
    };
};
module.exports = util;

Result Node.js v0.10.33:

$ node ./module-layout.js
Called util.yo("man") 1,000,000 times in 0.007074709 sec (2.87 x slower than v3)
Called util.yo("man") 10,000,000 times in 0.058642627 sec (4.61 x slower than v3)
Called util.yo("man") 100,000,000 times in 0.597742876 sec (5.03 x slower than v3)
Called util.yo("man") 1,000,000,000 times in 5.939687564 sec (5.09 x slower than v3)
Called util.yo("man") 10,000,000,000 times in 59.359191069 sec (4.35 x slower than v3)
Called util.yo("man") 100,000,000,000 times in 595.338973828 sec (4.98 x slower than v3)

Result Node.js v0.11.14:

$ node ./module-layout.js
Called util.yo("man") 1.000.000 times in 0.007510633 sec (3.05 x slower than v3)
Called util.yo("man") 10.000.000 times in 0.068987011 sec (5.54 x slower than v3)
Called util.yo("man") 100.000.000 times in 0.665280404 sec (5.44 x slower than v3)
Called util.yo("man") 1.000.000.000 times in 6.595546359 sec (5.64 x slower than v3)
Called util.yo("man") 10.000.000.000 times in 67.434427564 sec (4.91 x slower than v3)
Called util.yo("man") 100.000.000.000 times in 689.715061154 sec (5.60 x slower than v3)

Result Node.js v0.12.0:

$ node ./module-layout.js
Called v1, util.yo("man") 1.000.000 times in 0.007226692 sec (3.34 x slower than v3)
Called v1, util.yo("man") 10.000.000 times in 0.064195452 sec (6.12 x slower than v3)
Called v1, util.yo("man") 100.000.000 times in 0.621423802 sec (7.26 x slower than v3)
Called v1, util.yo("man") 1.000.000.000 times in 6.246142513 sec (7.16 x slower than v3)
Called v1, util.yo("man") 10.000.000.000 times in 64.54430814 sec (4.66 x slower than v3)
Called v1, util.yo("man") 100.000.000.000 times in 637.652944138 sec (5.23 x slower than v3)

Version 2

'use strict';
var app_path = __dirname + '/../',
    opts = {};

function Util(options) {
    opts = options || {};
}

Util.prototype.yo = function yo(message) {
    return message;
};
module.exports = Util;

Result Node.js v0.10.33:

$ node ./module-layout.js
Called util.yo("man") 1,000,000 times in 0.006080576 sec (2.46 x slower than v3)
Called util.yo("man") 10,000,000 times in 0.050371113 sec (3.95 x slower than v3)
Called util.yo("man") 100,000,000 times in 0.475688023 sec (4.00 x slower than v3)
Called util.yo("man") 1,000,000,000 times in 4.706555301 sec (4.04 x slower than v3)
Called util.yo("man") 10,000,000,000 times in 49.717816971 sec (3.64 x slower than v3)
Called util.yo("man") 100,000,000,000 times in 505.092472934 sec (4.22 x slower than v3)

Result Node.js v0.11.14:

$ node ./module-layout.js
Called util.yo("man") 1.000.000 times in 0.002497188 sec (1.02 x slower than v3)
Called util.yo("man") 10.000.000 times in 0.012380063 sec (0.99 x slower than v3)
Called util.yo("man") 100.000.000 times in 0.120457243 sec (0.98 x slower than v3)
Called util.yo("man") 1.000.000.000 times in 1.185202365 sec (1.01 x slower than v3)
Called util.yo("man") 10.000.000.000 times in 13.690612949 sec (1.00 x slower than v3)
Called util.yo("man") 100.000.000.000 times in 119.500660001 sec (0.97 x slower than v3)

Result Node.js v0.12.0:

$ node ./module-layout.js
Called v2, util.yo("man") 1.000.000 times in 0.002575107 sec (1.01 x slower than v3)
Called v2, util.yo("man") 10.000.000 times in 0.010718542 sec (0.99 x slower than v3)
Called v2, util.yo("man") 100.000.000 times in 0.092111598 sec (1.07 x slower than v3)
Called v2, util.yo("man") 1.000.000.000 times in 0.877508894 sec (1.02 x slower than v3)
Called v2, util.yo("man") 10.000.000.000 times in 13.182168687 sec (0.96 x slower than v3)
Called v2, util.yo("man") 100.000.000.000 times in 120.849870219 sec (1.00 x slower than v3)

Version 3

'use strict';
var app_path = __dirname + '/../';

function Util(options) {
    this.opts = options || {};
}

Util.prototype.yo = function yo(message) {
    return message;
};
module.exports = Util;

Result 0.10.33:

$ node ./module-layout.js
Called util.yo("man") 1,000,000 times in 0.002463224 sec
Called util.yo("man") 10,000,000 times in 0.012720038 sec
Called util.yo("man") 100,000,000 times in 0.118724084 sec
Called util.yo("man") 1,000,000,000 times in 1.164874462 sec
Called util.yo("man") 10,000,000,000 times in 13.641269154 sec
Called util.yo("man") 100,000,000,000 times in 119.461431737 sec

Result Node.js v0.11.14:

$ node ./module-layout.js
Called util.yo("man") 1.000.000 times in 0.00245936 sec
Called util.yo("man") 10.000.000 times in 0.012447459 sec
Called util.yo("man") 100.000.000 times in 0.122345599 sec
Called util.yo("man") 1.000.000.000 times in 1.168556993 sec
Called util.yo("man") 10.000.000.000 times in 13.714391182 sec
Called util.yo("man") 100.000.000.000 times in 123.129450625 sec

Result Node.js v0.12.0:

$ node ./module-layout.js
Called v3, util.yo("man") 1.000.000 times in 0.002398085 sec
Called v3, util.yo("man") 10.000.000 times in 0.010607878 sec
Called v3, util.yo("man") 100.000.000 times in 0.096416779 sec
Called v3, util.yo("man") 1.000.000.000 times in 0.921059746 sec
Called v3, util.yo("man") 10.000.000.000 times in 13.381309877 sec
Called v3, util.yo("man") 100.000.000.000 times in 120.432087601 sec

Summary

Slower than version 3Slower than version 3

Version 1 and 2 compared to version 3 in terms of how slow they are compared to the last version.

Node.js v0.10.33:

  • Version 3 is about 5 times faster than version 1.
  • Version 3 is about 4 times faster than version 2.

Node.js v0.11.14:

  • Version 3 is about 5 timer faster than version 1.
  • Version 3 is almost equal to version 2.

Node.js v0.12.0:

  • Version 3 is about 6 timer faster than version 1.
  • Version 3 is almost equal to version 2.

I've also made a couple of charts to visualize the differences in execution time.

Execution time 1-100 millExecution time 1-100 mill

1-100 million calls to the yo-function.

Execution time 1-100 billExecution time 1-100 bill

1-100 billion calls to the yo-function.

Why?

Version 1

Version 1 are returning an object literal which prevents the V8 from discovering the method properties returned. The V8 is then unable to promote the methods from the object it self to the object's hidden class. Due to this it can't be inlined by the optimizer and every call to it has to make an expensive call to the uninlined method. [1]

$ node --trace-inlining module-layout-v1.js 1000000000
[...snip...]
Did not inline util called from  (target contains unsupported syntax [early]).

The unsupported syntax is:

return {
        yo: function (message) {
            return message;
        }
    };

Version 2

Version 2 are using a module global variable opts to store options. This prevents the x64 version of V8 (used in node v0.10.x) from inlining the yo method. Every call to the yo method requires a context change. In newer versions of node the method is inlined and the performance is equally good as version 3. [1]

$ node -v
v0.10.33
$ node --trace-inlining module-layout-v2.js 1000000000
Did not inline parseInt called from  (target not inlineable).
Did not inline require called from  (target requires context change).
Did not inline Util called from  (target requires context change).
Did not inline yo called from  (target requires context change).

$ node -v
v0.12.0
$ node --trace-inlining module-layout-v2.js 1000000000
Inlined require called from .
Inlined Util called from .
Inlined yo called from .

Please feel free to comment below.

[1] Corrected after Vyacheslav Egorov's comment. He is a real V8 guru :)

Run scripts

Bash script runner:

#!/bin/bash
loopcnt=(1000000 10000000 100000000 1000000000 10000000000 100000000000)
echo "Version 1"
for i in "${loopcnt[@]}"
do
    node ./module-layout-v1.js $i
done

echo "Version 2"
for i in "${loopcnt[@]}"
do
    node ./module-layout-v2.js $i
done

echo "Version 3"
for i in "${loopcnt[@]}"
do
    node ./module-layout-v3.js $i
done

Version 1:

'use strict';
var app_path = __dirname + '/';
var loop_cnt = parseInt(process.argv[2]);

var util  = require(app_path + 'lib/util-v1')({ foo: 'bar' });
var start = process.hrtime();
for (var i = 0; i < loop_cnt; i++) {
    util.yo('man');
}
var end = process.hrtime(start);
var total = end[0] + end[1] / 1000000000;
console.log('Called v1, util.yo("man")', numberWithCommas(i), 'times in', total, 'sec');

function numberWithCommas(x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}

Version 2:

'use strict';
var app_path = __dirname + '/';
var loop_cnt = parseInt(process.argv[2]);

var Util  = require(app_path + 'lib/util-v2');
var util = new Util({ foo: 'bar' });

var start = process.hrtime();
for (var i = 0; i < loop_cnt; i++) {
    util.yo('man');
}
var end = process.hrtime(start);
var total = end[0] + end[1] / 1000000000;
console.log('Called v2, util.yo("man")', numberWithCommas(i), 'times in', total, 'sec');

function numberWithCommas(x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}

Version 3:

'use strict';
var app_path = __dirname + '/';
var loop_cnt = parseInt(process.argv[2]);

var Util  = require(app_path + 'lib/util-v3');
var util = new Util({ foo: 'bar' });

var start = process.hrtime();
for (var i = 0; i < loop_cnt; i++) {
    util.yo('man');
}
var end = process.hrtime(start);
var total = end[0] + end[1] / 1000000000;
console.log('Called v3, util.yo("man")', numberWithCommas(i), 'times in', total, 'sec');

function numberWithCommas(x) {
    return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}