4

How can I write functions that v8 will inline?

Are there any tools to pre-compile my code to statically inline some functions? To statically transform functions and function calls to avoid capturing values?


Background

I noticed that the bottleneck of a JS program I wrote was a very simple function call: I was calling the function in a loop iterating millions of times, and manually inlining the function (i.e. replacing the function with its code) sped up the code by a few orders of magnitude.

After that I tried to study the problem for a little, but couldn't infer rules on how function calls are optimized by v8, and how to write efficient functions.


Sample code: iterating 1 billion times

  1. incrementing a counter:

    let counter = 0;
    while(counter < 1e9) ++counter;
    

    it takes about ~1 sec, on my system, both on Google Chrome/Chromium and v8. ~14 secs iterating 1e10 times.

  2. assigning to the counter the value of an incrementing function:

    function incr(c) { return c+1; }
    let counter = 0;
    while(counter < 1e9) counter = incr(counter);
    

    it takes about ~1 sec. ~14 secs iterating 1e10 times.

  3. calling a function (declared only once) that increments the captured counter:

    let counter = 0;
    function incr() { ++counter; }
    while(counter < 1e9) incr();
    

    it takes about ~3 sec. ~98 secs iterating 1e10 times.

  4. calling a (arrow) function defined in the loop that increments the captured counter:

    let counter = 0;
    while(counter < 1e9) (()=>{ ++counter; })();
    

    it takes about ~24 secs. (I noticed that a named function or an arrow one makes no difference)

  5. calling a (arrow) function defined in the loop to increment the counter without capturing:

    let counter = 0;
    while(counter < 1e9) {
        const incr = (c)=>c+1;
        counter = incr(counter);
    }
    

    it takes about ~22 secs.

I'm surprised by the fact that:

  • capturing a variable slows down the code. Why? Is this a general rule? Should I always avoid capturing variables in performance critical functions?

  • the negative effects of capturing a variable grow a lot when iterating 1e10 times. What's going on there? If I had to take a wild guess I'd say that beyond 1^31 the variable changes type, and the function wasn't optimized for this?

  • declaring a function in a loop slows down the code so much. v8 doesn't optimize the function at all? I thought it was smarter than that! I guess I should never declare functions in critical loops...

  • it makes little difference if the function declared in a loop captures a variable or not. I guess capturing a variable is bad for optimized code, but not so bad for not optimized one?

  • given all of this, I'm actually surprised v8 can perfectly inline long-lasting non-capturing functions. I guess these are the only reliable ones performance-wise?


Edit 1: adding some extra snippets to expose extra weirdness.

I created a new file, with the following code inside:

const start = new Date();
function incr(c) { return c+1; }
let counter = 0;
while(counter < 1e9) counter = incr(counter);
console.log( new Date().getTime() - start.getTime() );

It prints a value closed to ~1 sec.

Then I declared a new variable at the end of the file. Any variable works fine: just append let x; to that snipped. The code now took ~12 secs to complete.

If instead of using that incr function you just use ++counter as in the very first snippet, the extra variable makes the performance degrade from ~1 sec to ~2.5 secs. Putting these snippets into functions, declaring other variables or changing the order of some statements sometimes improves the performance, while other times degrades it even further.

  • WTF?

  • I knew about weird effects like this one, and I've read a bunch of guides on how to optimize JS for v8. Still: WTF?!

  • I played for a bit with the bottleneck of the JS program that made me start this research. I saw a difference of more than 4 orders of magnitude between implementations that I wouldn't have expected to be any different. I'm currently convinced that the performance of number-crunching algorithms in v8 is completely unpredictable and am going to rewrite the bottleneck in C and expose it as a function to v8.

4
  • "Why? Is this a general rule?" --- it should lookup in the parent scopes Commented Jul 9, 2016 at 2:42
  • @zerkms: well, if not optimized. I wonder why the snippet 3 is not compiled into exactly the snippet 1 like it's happening with snippet 2. Commented Jul 9, 2016 at 2:45
  • You seem to be using the term "lambda function" to mean "arrow function". They're not the same thing. Commented Jul 13, 2016 at 15:01
  • @nnnnnn: right, I tend to use them as synonyms. I'm fixing the post. Commented Jul 13, 2016 at 15:15

1 Answer 1

1
  1. calling a (lambda) function defined in the loop that increments the captured counter
  2. calling a (lambda) function defined in the loop to increment the counter without capturing

why do you think, that creating 1 billion!!!!! identic functions in a loop, may be any good idea? Especially if you only call them once (inside this loop) and then trhow them away.

Actually I'm impressed on how efficient this insane task is handled by the v8-engine. I would have thought, that it would take at least a few minutes to perform that. Again: we're talking about creating 1 billion functions, and then calling them once.

the negative effects of capturing a variable grow a lot when iterating 1e10 times. What's going on there? If I had to take a wild guess I'd say that beyond 1^31 the variable changes type, and the function wasn't optimized for this?

right, beyond 1^31 it's no int32 anymore, but a 64-bit float, that you're with, and out of a sudden, the type has changed => the code get's deoptimized.

declaring a function in a loop slows down the code so much. v8 doesn't optimize the function at all? I thought it was smarter than that! I guess I should never use lambdas in critical loops

A function is considered for optimization after about 100-150 calls. It makes no sense to optimize every last function that's only called once, or twice.

it makes little difference if the function declared in a loop captures a variable or not. I guess capturing a variable is bad for optimized code, but not so bad for not optimized one?

Yes, accessing a captured variable takes a tiny bit longer than accessing a local variable, but that's not the point here; neither for optimized nor for non-optimized code. The point here is still that you create 1 billion functions in a loop.

conclusion: create the function once before the loop, and then call it in the loop. Then it should not have any significant performance-impact wether you're passing or capturing the variables.

Sign up to request clarification or add additional context in comments.

3 Comments

Do you have any source, or did you draw your conclusions from my snippets and my conclusions? I added a new paragraph to the question, showing how I can cripple the performance by a factor of 12x (or 2.5x if not using a function) just by declaring an extra variable that isn't even used in the loop. I've spent quite some time tinkering with the piece of code where I found the bottleneck that made me start all of this, and I've seen lots of weirdness that I can't explain with your answer.
Besides some times is kind of necessary to declare a lambda function inside a loop, for instance if you need to do something like this: loop((x)=>{ f(()=>{ g(x); }); }); (i.e. inside a loop you need to create a callback that uses one of the variables declared in the scope of the loop) - which is something that to me happens quite often, especially with asynchronous or functional-style libraries. Sure you can try hard and find a hacky solution (e.g. using a global variable - as long as nothing is asynchronous), but that complicates things a lot and in any case v8 should be able to optimize.
the answer is on the point, but a little emotional to my taste

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.