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

timers: improve setTimeout/Interval performance #8661

Merged

Conversation

mscdex
Copy link
Contributor

@mscdex mscdex commented Sep 20, 2016

Checklist
  • make -j4 test (UNIX), or vcbuild test nosign (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)
  • timers
Description of change

This commit improves timers performance by making functions inlineable and avoiding the creation of extra closures/functions.

This commit also makes setTimeout/Interval argument handling consistent with that of setImmediate.

These changes give ~22% improvement in the existing 'breadth' timers benchmark.

@mscdex mscdex added the timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout. label Sep 20, 2016
@mscdex
Copy link
Contributor Author

mscdex commented Sep 20, 2016

@@ -94,8 +94,8 @@ const TIMEOUT_MAX = 2147483647; // 2^31-1
//
// - key = time in milliseconds
// - value = linked list
const refedLists = {};
const unrefedLists = {};
const refedLists = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember we had a long discussion about this, I think for http. Have we ever come to a conclusion whether this is a good idea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The objects here are not exposed, so it's not an issue in this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. And it does improve performance? Because - at least for me - it decreases readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't measure it specifically, but it does remove unnecessary hash table entries. It may also help if V8 sees only numeric keys being set on the object?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fhinkel Just curious. Do you mean using a Map instead of Object.create? #7581 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it increases readability, but please check performance - I recall there being a regression when using .create(null) at one point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Florian-R Thanks, that's the discussion I was thinking of. But in this case I meant {} vs .create(null).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamingr I'm not sure what regression you're referring to. These objects are created at startup, not during runtime, so the time to create them doesn't matter as much.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand this is not about performance, but just out of curiosity, I measured the performance difference.

console.time("{}"); for (var i = 100000; i--;) { var a = {}; } console.timeEnd("{}"); var create = Object.create; console.time("create"); for (var i = 100000; i--;) { var b = create(null); } console.timeEnd("create");

{}: 5.792ms
create: 15.221ms

Probably, calling a function is heavier than {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literals are always faster than instantiation and builder function calls, in JS and most languages.

@@ -317,51 +322,76 @@ exports.enroll = function(item, msecs) {
*/


exports.setTimeout = function(callback, after) {
exports.setTimeout = function(callback, after, arg1, arg2, arg3) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I like this much better than using arguments.


if (!(after >= 1 && after <= TIMEOUT_MAX)) {
after = 1; // schedule on next tick, follows browser behaviour
var len = arguments.length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check for typeof args1 == 'undefined' rather than using length? Or is undefined a valid value for args1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it's a valid value.

Copy link
Member

@fhinkel fhinkel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the speedup. LGTM.

Copy link
Member

@benjamingr benjamingr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me. Left one comment but it should not block merging.

@@ -94,8 +94,8 @@ const TIMEOUT_MAX = 2147483647; // 2^31-1
//
// - key = time in milliseconds
// - value = linked list
const refedLists = {};
const unrefedLists = {};
const refedLists = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it increases readability, but please check performance - I recall there being a regression when using .create(null) at one point.

}

L.append(list, item);
assert(!L.isEmpty(list)); // list is not empty
}

function createTimersList(msecs, unrefed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure why we'd want this in an extra function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think extracting it into a function increases readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is to keep the insert() size down, but it does also help a bit with readability I suppose.

@@ -317,51 +322,76 @@ exports.enroll = function(item, msecs) {
*/


exports.setTimeout = function(callback, after) {
exports.setTimeout = function(callback, after, arg1, arg2, arg3) {
Copy link
Contributor

@Fishrock123 Fishrock123 Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...args ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rest args are still slow.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great move.


if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) {
function createRepeatTimeout(callback, repeat, args) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a new function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep function sizes down.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, there's no repetead call, you add a short flow path (and creates a mini and fake cyclomatic complexity), function is in the same script and right under the function (so you just added brackets and more names to read). Function call is slower than direct structured code (and even minimal performance gains, in this call stack queen, is important).

I would not do this separation.

timer._onTimeout = wrapper;
timer._repeat = ontimeout;
var timer = new Timeout(repeat, callback, args);
timer._repeat = repeat;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsure if changing this won't have side-effects somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I grepped lib/ and ._repeat isn't used anywhere except timers.js.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more concerned about user code, I do understand that it is unlikely but it has been the way it was an awfully long time in the wild.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you rather I leave the variable set to null since it wouldn't be used anymore and add a new property that stores the interval value?


function rearm(timer) {
// If timer is unref'd (or was - it's permanently removed from the list.)
if (timer._handle && timer instanceof Timeout) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should only be timer._handle I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not, as _http_outgoing and net have a ._handle and manually enroll() pre-made objects. Node will crash since those ._handle instances are not only not Timeout instances, but they also do not have a .start() method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the reason this change is necessary now is that the rearming code no longer has access to the scope where the Timeout instance was created.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmmm ok

callback.apply(timer, args);
}
}
if (timer._repeat != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would just check if (timer._repeat) iirc that is what is done elsewhere.

Copy link
Contributor Author

@mscdex mscdex Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's safe really since ._repeat is now being used to store the interval value, which would not work for the 0 interval.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex you can't have an interval of 0. It becomes 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

const refedLists = {};
const unrefedLists = {};
const refedLists = Object.create(null);
const unrefedLists = Object.create(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any tangible benefit to doing this.

We only use Number value keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As was previously mentioned, I didn't measure the change specifically, but it does remove unnecessary hash table entries. It may also help if V8 sees only numeric keys being set on the object? It has nothing to do with key name collisions in this case.

Copy link
Member

@jasnell jasnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -229,7 +234,7 @@ function tryOnTimeout(timer, list) {
timer._called = true;
var threw = true;
try {
timer._onTimeout();
ontimeout(timer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ontimeout() throws will it deo-opt that function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which function? tryOnTimeout()? ontimeout()? The user's callback?

I don't know that throwing causes a deopt or not, but adding a try-* block certainly does cause a deopt for the containing function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but adding a try-* block certainly does cause a deopt for the containing function.

Right, which is why it was constrained to within one separate function.

I'm curious if a throw fork a user callback de-opts ontimeout() along the way though, since that could be somewhat unpleasant.

Maybe we shouldn't worry about it? Idk the sate of benchmarking this stuff is already so bad 😐

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wrote a little test and as far as I can tell a function (or any calling functions in the call stack) shouldn't get deopted if that function throws.

Copy link

@leodutra leodutra Oct 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try deopt any container function. Node.js should, IMHO, always test for nullity and use flags/optionals and never try-catch in the inner functions... This pattern is not really a requirement in functional programming and there's huge performance benefits on native calls (basically, in any platform too. try-catch system is expensive).

timer._onTimeout = wrapper;
timer._repeat = ontimeout;
var timer = new Timeout(repeat, callback, args);
timer._repeat = repeat;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more concerned about user code, I do understand that it is unlikely but it has been the way it was an awfully long time in the wild.

args = [arg1, arg2];
} else if (len > 4) {
args = [arg1, arg2, arg3];
for (var i = 5; i < arguments.length; i++)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably doesn't matter much, but you could use len here instead of arguments.length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

@mscdex mscdex force-pushed the timers-improve-settimeout-interval-perf branch from b8fcf3d to c6268a7 Compare September 21, 2016 06:37
@mscdex
Copy link
Contributor Author

mscdex commented Sep 21, 2016

CI again after recent changes: https://ci.nodejs.org/job/node-test-pull-request/4188/

@benjamingr
Copy link
Member

Still LGTM.

@jasnell
Copy link
Member

jasnell commented Sep 21, 2016

@mscdex ... it would also be good to have a citgm run on this.

@mscdex
Copy link
Contributor Author

mscdex commented Sep 21, 2016

@mscdex
Copy link
Contributor Author

mscdex commented Sep 21, 2016

citgm looks ok, osx and ppcle had a few npm install issues it seems though but that does not appear to be related to this PR.

Copy link
Member

@imyller imyller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mscdex
Copy link
Contributor Author

mscdex commented Sep 29, 2016

@Fishrock123 Thoughts?

@Fishrock123
Copy link
Contributor

seems fine to me

@mscdex mscdex force-pushed the timers-improve-settimeout-interval-perf branch from c6268a7 to 92c3623 Compare October 1, 2016 05:51
@mscdex
Copy link
Contributor Author

mscdex commented Oct 1, 2016

One last CI before landing: https://ci.nodejs.org/job/node-test-pull-request/4347/

EDIT: CI is green

This commit improves timers performance by making functions
inlineable and avoiding the creation of extra closures/functions.

This commit also makes setTimeout/Interval argument handling
consistent with that of setImmediate.

These changes give ~22% improvement in the existing 'breadth' timers
benchmark.

PR-URL: nodejs#8661
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ilkka Myller <ilkka.myller@nodefield.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
@mscdex mscdex force-pushed the timers-improve-settimeout-interval-perf branch from 92c3623 to c8c2544 Compare October 1, 2016 07:01
@mscdex mscdex merged commit c8c2544 into nodejs:master Oct 1, 2016
@mscdex mscdex deleted the timers-improve-settimeout-interval-perf branch October 1, 2016 07:03
jasnell pushed a commit that referenced this pull request Oct 6, 2016
This commit improves timers performance by making functions
inlineable and avoiding the creation of extra closures/functions.

This commit also makes setTimeout/Interval argument handling
consistent with that of setImmediate.

These changes give ~22% improvement in the existing 'breadth' timers
benchmark.

PR-URL: #8661
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ilkka Myller <ilkka.myller@nodefield.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
@MylesBorins
Copy link
Contributor

adding LTS watch for now, although it will need quite a bit of time to bake before landing.

@mscdex do you see any reason we should not land it in the future?

@mscdex
Copy link
Contributor Author

mscdex commented Oct 7, 2016

@thealphanerd No, it should be ok.

Fishrock123 pushed a commit that referenced this pull request Oct 11, 2016
This commit improves timers performance by making functions
inlineable and avoiding the creation of extra closures/functions.

This commit also makes setTimeout/Interval argument handling
consistent with that of setImmediate.

These changes give ~22% improvement in the existing 'breadth' timers
benchmark.

PR-URL: #8661
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ilkka Myller <ilkka.myller@nodefield.com>
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Fishrock123 added a commit that referenced this pull request Oct 12, 2016
* fs:
  - `SyncWriteStream` now inherits from `Stream.Writable`. (Anna
Henningsen) #8830
    - Practically, this means that when stdio is piped to a file,
stdout and stderr will still be `Writable` streams.
  - `fs.existsSync()` has been undeprecated. `fs.exists()` remains
deprecated. (Dan Fabulich) #8364
* http: `http.request()` now accepts a `timeout` option. (Rene Weber)
#8101
* module: The module loader now maintains its own realpath cache. (Anna
Henningsen) #8100
* npm: Upgraded to 3.10.8 (Kat Marchán)
#8706
* stream: `Duplex` streams now show proper `instanceof
Stream.Writable`. (Anna Henningsen)
#8834
* timers: Improved `setTimeout`/`Interval` performance by up to 22%.
(Brian White) #8661

PR-URL: #9034
Fishrock123 added a commit that referenced this pull request Oct 12, 2016
* fs:
  - `SyncWriteStream` now inherits from `Stream.Writable`. (Anna
Henningsen) #8830
    - Practically, this means that when stdio is piped to a file,
stdout and stderr will still be `Writable` streams.
  - `fs.existsSync()` has been undeprecated. `fs.exists()` remains
deprecated. (Dan Fabulich) #8364
* http: `http.request()` now accepts a `timeout` option. (Rene Weber)
#8101
* module: The module loader now maintains its own realpath cache. (Anna
Henningsen) #8100
* npm: Upgraded to 3.10.8 (Kat Marchán)
#8706
* stream: `Duplex` streams now show proper `instanceof
Stream.Writable`. (Anna Henningsen)
#8834
* timers: Improved `setTimeout`/`Interval` performance by up to 22%.
(Brian White) #8661

PR-URL: #9034
imyller added a commit to imyller/meta-nodejs that referenced this pull request Oct 13, 2016
    * fs:
      - `SyncWriteStream` now inherits from `Stream.Writable`. (Anna
    Henningsen) nodejs/node#8830
        - Practically, this means that when stdio is piped to a file,
    stdout and stderr will still be `Writable` streams.
      - `fs.existsSync()` has been undeprecated. `fs.exists()` remains
    deprecated. (Dan Fabulich) nodejs/node#8364
    * http: `http.request()` now accepts a `timeout` option. (Rene Weber)
    nodejs/node#8101
    * module: The module loader now maintains its own realpath cache. (Anna
    Henningsen) nodejs/node#8100
    * npm: Upgraded to 3.10.8 (Kat Marchan)
    nodejs/node#8706
    * stream: `Duplex` streams now show proper `instanceof
    Stream.Writable`. (Anna Henningsen)
    nodejs/node#8834
    * timers: Improved `setTimeout`/`Interval` performance by up to 22%.
    (Brian White) nodejs/node#8661

Signed-off-by: Ilkka Myller <ilkka.myller@nodefield.com>
imyller added a commit to imyller/meta-nodejs that referenced this pull request Oct 13, 2016
    * fs:
      - `SyncWriteStream` now inherits from `Stream.Writable`. (Anna
    Henningsen) nodejs/node#8830
        - Practically, this means that when stdio is piped to a file,
    stdout and stderr will still be `Writable` streams.
      - `fs.existsSync()` has been undeprecated. `fs.exists()` remains
    deprecated. (Dan Fabulich) nodejs/node#8364
    * http: `http.request()` now accepts a `timeout` option. (Rene Weber)
    nodejs/node#8101
    * module: The module loader now maintains its own realpath cache. (Anna
    Henningsen) nodejs/node#8100
    * npm: Upgraded to 3.10.8 (Kat Marchan)
    nodejs/node#8706
    * stream: `Duplex` streams now show proper `instanceof
    Stream.Writable`. (Anna Henningsen)
    nodejs/node#8834
    * timers: Improved `setTimeout`/`Interval` performance by up to 22%.
    (Brian White) nodejs/node#8661

Signed-off-by: Ilkka Myller <ilkka.myller@nodefield.com>
Copy link

@leodutra leodutra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some point to enhance. Object.create is bad at that first lines (literal is faster than Object.create() and I do not see real memory benefits).

There's space for micro optimizations, but the overall improvements are great and makes the commit rich enough.

@@ -94,8 +94,8 @@ const TIMEOUT_MAX = 2147483647; // 2^31-1
//
// - key = time in milliseconds
// - value = linked list
const refedLists = {};
const unrefedLists = {};
const refedLists = Object.create(null);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literals are always faster than instantiation and builder function calls, in JS and most languages.

@@ -229,7 +234,7 @@ function tryOnTimeout(timer, list) {
timer._called = true;
var threw = true;
try {
timer._onTimeout();
ontimeout(timer);
Copy link

@leodutra leodutra Oct 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try deopt any container function. Node.js should, IMHO, always test for nullity and use flags/optionals and never try-catch in the inner functions... This pattern is not really a requirement in functional programming and there's huge performance benefits on native calls (basically, in any platform too. try-catch system is expensive).

@@ -317,51 +322,76 @@ exports.enroll = function(item, msecs) {
*/


exports.setTimeout = function(callback, after) {
exports.setTimeout = function(callback, after, arg1, arg2, arg3) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great move.


if (!(repeat >= 1 && repeat <= TIMEOUT_MAX)) {
function createRepeatTimeout(callback, repeat, args) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO, there's no repetead call, you add a short flow path (and creates a mini and fake cyclomatic complexity), function is in the same script and right under the function (so you just added brackets and more names to read). Function call is slower than direct structured code (and even minimal performance gains, in this call stack queen, is important).

I would not do this separation.

this._repeat = null;
}


function unrefdHandle() {
this.owner._onTimeout();
ontimeout(this.owner);
Copy link

@leodutra leodutra Oct 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always better for internal workings. ALWAYS, Node community.
Avoid this, globals, function calls, function separation in minimal pieces and inner functions... and we'll always have fine code with max JS performance.

@nodejs nodejs locked and limited conversation to collaborators Oct 14, 2016
@Fishrock123
Copy link
Contributor

@leodutra this was already merged. If you want, you could submit a new PR.

@jbergstroem
Copy link
Member

I think performance-related improvements to try/catch/finally landed in v8 54.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
timers Issues and PRs related to the timers subsystem / setImmediate, setInterval, setTimeout.
Projects
None yet
Development

Successfully merging this pull request may close these issues.