JavaScript Call Performance – Just Inline It
Tuesday, January 25th, 2011 by Sebastian MarkbågeThere are a couple of well-known micro optimization techniques when you need that extra speed within an expensive loop. It generally comes down to eliminating function calls and flattening the closure scope.
Intuitively you don’t want to bloat your download size with repeated code. You also want a clear separation of concerns to make your code understandable and managable. You want to separate common code into sub-routines (functions).
Intra-object method calls
I’ve done a number of tests on repeated calls within a single object. This is typically where an expensive loop would occur.
Now you might think that property look-ups are expensive and should be avoided. Since in a naive implementation they would be continued hash-table look-ups. However, traversing closure scopes and .call(bind) invocations tend to be more expensive in real world engines. Generally, you are better off invoking the function, by property name, on the same prototype – continuously. Engines optimize for this pattern. In the tests you can see that this is even more prevalent in recent optimizations.
Super/base method calls
In Class-oriented/OOP patterns, you often see the need to override an inherited method. However, while doing so you wish to call the overridden (super/base/parent) function from within the new one.
The most common way of doing this is by storing a reference to the prototype object, then invoking the function using .call() or .apply() to set the “this” context to the right value. However, as I’ve shown, navigating a closure and invoking the function using .call() is more expensive than invoking prototype methods.
The benefit of storing a reference to the prototype is that you get a live property that can be monkey patched with a new super function on the fly. However, relying on this pattern can be tricky.
You’re better off having a module system that supports proper file ordering of monkey patches. Where monkey patches are applied before any child modules inherit from their parent. This will enable better optimization on engines that depend on declaration order to optimize their hidden classes (like V8). It also enables monkey patching of mixins that are copied. I.e. has no live inheritance (since JS doesn’t support multiple inheritance).
If we can assume that we don’t need on-the-fly updates of super functions, then we can optimize this further. Again, my tests show, that it’s faster to invoke a method on the same prototype than finding a function reference and invoking .call().
I’ve toyed with some syntax experiments to make this prettier.
Just Inline It
Daniel Steigerwald pointed out that once you make the assumption that we don’t need on-the-fly monkey patches, you should just inline the code. This is always faster.
Let’s take a look at some nonsensical code. The function bar calls the function foo four times.
function foo(state){ var my = 'code'; for (var i = 0, l = my.length; i < l; i++) if (i % 10 == 2) state.push(my[i]); else if (i == 1) state.push(my[0]); else if (l == 2 && i == 3) state.push('foo'); else state.push('else'); return my; } function bar(condition, state){ var complexCondition = !condition && state.length == 3; var value = state[0]; while (condition){ value = foo(state); if (complexCondition){ state.push('someData'); value = foo(state); } else if (value.length == 5){ value = foo([]); } else if (value == 'else'){ value = foo(state) state.push('test'); } condition = (value == 'foo'); } } |
This file weighs in at about 300 bytes gzipped.
If we instead inline (copy) the foo code into all four places in the bar function. We get something like this.
function bar(condition, state){ var complexCondition = !condition && state.length == 3; var value = state[0]; while (condition){ var stack = state; var my = 'code'; for (var i = 0, l = my.length; i < l; i++) if (i % 10 == 2) stack.push(my[i]); else if (i == 1) stack.push(my[0]); else if (l == 2 && i == 3) stack.push('foo'); else stack.push('else'); value = my; if (complexCondition){ state.push('someData'); var stack = state; var my = 'code'; for (var i = 0, l = my.length; i < l; i++) if (i % 10 == 2) stack.push(my[i]); else if (i == 1) stack.push(my[0]); else if (l == 2 && i == 3) stack.push('foo'); else stack.push('else'); value = my; } else if (value.length == 5){ var stack = []; var my = 'code'; for (var i = 0, l = my.length; i < l; i++) if (i % 10 == 2) stack.push(my[i]); else if (i == 1) stack.push(my[0]); else if (l == 2 && i == 3) stack.push('foo'); else stack.push('else'); value = my; } else if (value == 'else'){ var stack = state; var my = 'code'; for (var i = 0, l = my.length; i < l; i++) if (i % 10 == 2) stack.push(my[i]); else if (i == 1) stack.push(my[0]); else if (l == 2 && i == 3) stack.push('foo'); else stack.push('else'); value = my; state.push('test'); } condition = (value == 'foo'); } } |
This file weighs about 290 bytes gzipped. Wait, what? We more than doubled the original file size but the result is smaller?
Yes, the GZIP compression uses the LZ77 algorithm to find repeated byte sequences. This means that inlining your code may actually result in smaller download sizes since some plumbing code is removed.
Is it cheating to compare gzipped file size instead of originals? No. You should always send your static uncompressed content using the GZIP compression. It’s cheap. This is the real world baseline. You should optimize for this.
There are some quirks to look out for. Some minifiers rename variables inconsistently. This may result in inconsistent sequences. You should look out for this, since that would cause a larger file size.
The larger code base may cause a slightly slower start up, since the JS engine needs to parse and compile more code. This should be very very minimal though. Remember, we’re optimizing for tight loops here.
JS-to-JS Compilers
Our inlined code is ugly and unmaintainable. Luckily there are tools that can inline pretty source code for us. E.g. the Google Closure Compiler. Unfortunately the Closure Compiler doesn’t seem to inline functions that are called more than once. Neither does it use these optimization techniques for super calls. Perhaps there are settings that I’m not aware of.
Recursive functions may be difficult to inline but there are tools that can rewrite these and do tail call optimizations as well.
However, this is just a hint of the awesomeness that could be achieved by using a custom JS-to-JS compiler. More on that later.
Tags: Classes, Compiler, Inlining, JavaScript, Optimization, Performance
January 25th, 2011 at 01:23
[...] This post was mentioned on Twitter by Sebastian Markbåge and Sebastian Markbåge, Arian Stolwijk. Arian Stolwijk said: RT @sebmarkbage: JavaScript Optimization Blog Post: Function Calls. Just Inline It. http://goo.gl/FFQQu [...]
February 2nd, 2011 at 03:20
What makes you think that the Closure Compiler won’t inline a function that is use more than once?
May 18th, 2011 at 13:50
I loved this article!!! Appreciation!…
May 20th, 2011 at 01:08
I’m impressed, I have to admit. In fact rarely do I come across a site that’s educative and also fun, and I want to tell you, you may have hit the nail on the head. The idea is great; the difficulty is something that not sufficient individuals are speaking smartly about. I’m very satisfied that I came throughout this in my search for something referring to this.
June 7th, 2011 at 21:35
hot iscell bouta chill the rest of the day with the loves any1 want to join ?|kutiebae13|
December 26th, 2011 at 17:15
So, it seems the closure compiler does inline functions called more than once only if they are shorter than the function call itself. I have several one-liner utility functions I use which it inlined without trouble.
I was working with a program that had a 10 line chunk of code I really needed inlined. (it just performed some math operations, so the cost of the function invoke was far greater than work it did in the body). Since it was called in multiple locations, and is quite long, it was not getting inlined.
I ended up checking out a copy of the source from svn. Then commented out the call to trimCanidatesUsingOnCost (in InlineFunctions.java), and built a fresh copy. That seemed to do the trick.
March 9th, 2012 at 04:28
Apple featuring Rhapsody just as one app, this is a great start, but it can be currently hampered by the inability to store locally on your own iPod, and possesses a dismal 64kbps bit rate. When this changes, that will somewhat negate this advantage with the Zune, but the 10 songs per thirty days it is still a major also in Zune Pass’ favor.
June 30th, 2012 at 23:23
We’re a bunch of volunteers and opening a brand new scheme in our community. Your web site provided us with helpful info to work on. You’ve done a formidable task and our entire community will likely be grateful to you.
September 11th, 2012 at 15:08
Great post. Thanks a lot.
January 19th, 2013 at 20:59
Love the voice behind the rant. As a psychotherapist, xteacher, author and mother of three HG adults, vent away. People (especially bureaucrats) need to hear that raising these kids isn’t a slam dunk just because they are so capable they accommodate to significant challenges. Parents and kids need support all through their uniquely asynchronous lives. And they don’t need people to pathologize and stigmatize that which is “normal” in this population…namely intensity and complex thinking, feeling and relating to EVERYTHING. Enjoy your gifted kids, they grow up fast. Linda
January 30th, 2013 at 12:46
Hiya. Self-same trivial web site!! Guy .. Beautiful .. Superb .. I will bookmark your web site and engage the feeds additionally…I am jovial to locate thus a lot effective in rank here surrounded by the article. Thankfulness for sharing…
January 30th, 2013 at 16:00
A formidable share, I just given this onto a colleague who was doing a bit of evaluation on this. And he the truth is bought me breakfast because I found it for him.. smile. So let me reword that: Thnx for the deal with! However yeah Thnkx for spending the time to debate this, I really feel strongly about it and love studying more on this topic. If potential, as you develop into expertise, would you thoughts updating your blog with more details? It’s highly helpful for me. Large thumb up for this blog publish!
January 30th, 2013 at 17:01
Dead pent content material , regards for information .
April 17th, 2013 at 11:34
I just have these Miu Miu Bags last month. I prefer these people! They’re just beautiful together with cute. I just just didn’t like that little proper Miu Miu Bags, i possibly could check out the appear to be or something that is clear away little ft. All the left behind Miu Miu Bags doesnt do that while.
May 6th, 2013 at 11:59
article that appeared good enough to be read so that adds to knowledge when reading