peoro
peoro

Reputation: 26060

v8/chrome/node.js function inline

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:


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.

Upvotes: 4

Views: 1590

Answers (1)

Thomas
Thomas

Reputation: 12637

  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.

Upvotes: 1

Related Questions