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
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
1e10times.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
1e10times.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
1e10times.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)
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.