Skip to content

Commit

Permalink
timers: optimize setImmediate()
Browse files Browse the repository at this point in the history
Save the setImmediate() callback arguments into an array instead of a
closure, and invoke the callback on the arguments from an optimizable
function.

  60% faster setImmediate with 0 args (15% if self-recursive)
  4x faster setImmediate with 1-3 args, 2x with > 3
  seems to be faster with less memory pressure when memory is tight

Changes:
- use L.create() to build faster lists
- use runCallback() from within tryOnImmediate()
- save the arguments and do not build closures for the callbacks

PR-URL: nodejs#6436
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
  • Loading branch information
Andras authored and Fishrock123 committed Jun 29, 2016
1 parent 6b0f86a commit 6f75b66
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 35 deletions.
2 changes: 1 addition & 1 deletion lib/internal/linkedlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ exports.init = init;

// create a new linked list
function create() {
var list = { _idleNext: null, _idlePrev: null };
const list = { _idleNext: null, _idlePrev: null };
init(list);
return list;
}
Expand Down
79 changes: 47 additions & 32 deletions lib/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ var immediateQueue = L.create();


function processImmediate() {
var queue = immediateQueue;
const queue = immediateQueue;
var domain, immediate;

immediateQueue = L.create();
Expand All @@ -515,9 +515,13 @@ function processImmediate() {
immediate = L.shift(queue);
domain = immediate.domain;

if (!immediate._onImmediate)
continue;

if (domain)
domain.enter();

immediate._callback = immediate._onImmediate;
tryOnImmediate(immediate, queue);

if (domain)
Expand All @@ -538,7 +542,8 @@ function processImmediate() {
function tryOnImmediate(immediate, queue) {
var threw = true;
try {
immediate._onImmediate();
// make the actual call outside the try/catch to allow it to be optimized
runCallback(immediate);
threw = false;
} finally {
if (threw && !L.isEmpty(queue)) {
Expand All @@ -552,67 +557,77 @@ function tryOnImmediate(immediate, queue) {
}
}

function runCallback(timer) {
const argv = timer._argv;
const argc = argv ? argv.length : 0;
switch (argc) {
// fast-path callbacks with 0-3 arguments
case 0:
return timer._callback();
case 1:
return timer._callback(argv[0]);
case 2:
return timer._callback(argv[0], argv[1]);
case 3:
return timer._callback(argv[0], argv[1], argv[2]);
// more than 3 arguments run slower with .apply
default:
return timer._callback.apply(timer, argv);
}
}

function Immediate() { }

Immediate.prototype.domain = undefined;
Immediate.prototype._onImmediate = undefined;
Immediate.prototype._idleNext = undefined;
Immediate.prototype._idlePrev = undefined;

function Immediate() {
// assigning the callback here can cause optimize/deoptimize thrashing
// so have caller annotate the object (node v6.0.0, v8 5.0.71.35)
this._idleNext = null;
this._idlePrev = null;
this._callback = null;
this._argv = null;
this._onImmediate = null;
this.domain = process.domain;
}

exports.setImmediate = function(callback, arg1, arg2, arg3) {
if (typeof callback !== 'function') {
throw new TypeError('"callback" argument must be a function');
}

var i, args;
var len = arguments.length;
var immediate = new Immediate();

L.init(immediate);

switch (len) {
switch (arguments.length) {
// fast cases
case 0:
case 1:
immediate._onImmediate = callback;
break;
case 2:
immediate._onImmediate = function() {
callback.call(immediate, arg1);
};
args = [arg1];
break;
case 3:
immediate._onImmediate = function() {
callback.call(immediate, arg1, arg2);
};
args = [arg1, arg2];
break;
case 4:
immediate._onImmediate = function() {
callback.call(immediate, arg1, arg2, arg3);
};
args = [arg1, arg2, arg3];
break;
// slow case
default:
args = new Array(len - 1);
for (i = 1; i < len; i++)
args = [arg1, arg2, arg3];
for (i = 4; i < arguments.length; i++)
// extend array dynamically, makes .apply run much faster in v6.0.0
args[i - 1] = arguments[i];

immediate._onImmediate = function() {
callback.apply(immediate, args);
};
break;
}
// declaring it `const immediate` causes v6.0.0 to deoptimize this function
var immediate = new Immediate();
immediate._callback = callback;
immediate._argv = args;
immediate._onImmediate = callback;

if (!process._needImmediateCallback) {
process._needImmediateCallback = true;
process._immediateCallback = processImmediate;
}

if (process.domain)
immediate.domain = process.domain;

L.append(immediateQueue, immediate);

return immediate;
Expand Down
6 changes: 6 additions & 0 deletions test/parallel/test-timers-immediate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var assert = require('assert');
let immediateA = false;
let immediateB = false;
let immediateC = [];
let immediateD = [];

setImmediate(function() {
try {
Expand All @@ -25,8 +26,13 @@ setImmediate(function(x, y, z) {
immediateC = [x, y, z];
}, 1, 2, 3);

setImmediate(function(x, y, z, a, b) {
immediateD = [x, y, z, a, b];
}, 1, 2, 3, 4, 5);

process.on('exit', function() {
assert.ok(immediateA, 'Immediate should happen after normal execution');
assert.notStrictEqual(immediateB, true, 'immediateB should not fire');
assert.deepStrictEqual(immediateC, [1, 2, 3], 'immediateC args should match');
assert.deepStrictEqual(immediateD, [1, 2, 3, 4, 5], '5 args should match');
});
4 changes: 2 additions & 2 deletions test/parallel/test-timers-linked-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ assert.equal(C, L.shift(list));
// list
assert.ok(L.isEmpty(list));

var list2 = L.create();
var list3 = L.create();
const list2 = L.create();
const list3 = L.create();
assert.ok(L.isEmpty(list2));
assert.ok(L.isEmpty(list3));
assert.ok(list2 != list3);

0 comments on commit 6f75b66

Please sign in to comment.