Martyboy
Martyboy

Reputation: 370

Performance of declaring function withing a scope of a function vs outside of it

I was pondering on performance implications on whether or not to declare a function within a function scope vs outside of the scope.

To do that, I created a test using jsperf and the results were interesting to me and I'm hoping if someone can explain what is going on here.

Test: https://jsperf.com/saarman-fn-scope/1

Google Chrome results: chrome results

Microsoft Edge results: edge results

Firefox results: enter image description here

Upvotes: 6

Views: 1401

Answers (3)

jmrk
jmrk

Reputation: 40501

V8 developer here. In short: you've fallen victim to the traps of micro-benchmarking. Realistically, "Test 1" is slightly more efficient, but depending on your overall program the difference may well be too small to matter.

The reason "Test 1" is more efficient is because it creates fewer closures. Think of it as:

let mathAdd = new Function(...);
for (let i = 0; i < 1000; i++) {
  mathAdd();
}

vs.

for (let i = 0; i < 1000; i++) {
  let mathAdd = new Function(...);
  mathAdd();
}

Just as if you were calling new Object() or new MyFunkyConstructor(), it's more efficient to do that only once outside of the loop, rather than on every iteration.

The reason "Test 1" appears to be slower is an artifact of the test setup. The specific way how jsperf.com happens to wrap your code into functions under the hood happens to defeat V8's inlining mechanism in this case [1]. So in "Test 1", run is inlined, but mathAdd is not, so an actual call is performed, and an actual addition. In "Test 2", on the other hand, both run and mathAdd get inlined, the compiler subsequently sees that the results are not used, and eliminates all the dead code, so that you are benchmarking an empty loop: it creates no functions, calls no functions, and performs no addition (except for i++).

Feel free to inspect the generated assembly code to see it for yourself :-) In fact, if you want to create further microbenchmarks, you should get used to inspecting the assembly code, to make sure that the benchmark measures what you think it's measuring.

[1] I'm not sure why; if I had to guess: there's probably special handling to detect the fact that while run is a new closure every time the test case runs, it's always the same code underneath, but it looks like that special-casing only applies to functions in the local scope of the call, not to loads from the context chain as in the runmathAdd call. If that guess is correct, you could call it a bug (which Firefox apparently doesn't have); on the other hand, if the only impact is that dead-code elimination in microbenchmarks doesn't work any more, then it's certainly not an important issue to fix.

Upvotes: 12

coagmano
coagmano

Reputation: 5671

I believe what's happening in the Chrome and Firefox case is that it's inlining the mathAdd function. Because it's a simple function with no side effects that is both created and called within the function, the compiler replaces the call site with the internal code of the function.

The resulting code will look like this:

const run = count => {
  return 10 + count
}

for (let count = 0; count < 1000; count++) {
  run(count)
}

This saves the runtime a function declaration, and a new stack frame when the function is called.

I suspect that in the case where the functions are separate, the compiler can't guarantee that it's safe to inline, and you end up paying the cost of a new stack frame and function call each time run is called.


I did some more tests: https://jsperf.com/saarman-fn-scope/5

Moving the code of Test2 into the loop (All in loop), I expected the compiler to inline the function call because it's block scoped to the loop and contains very little code. This expectation was wrong, but it's still faster than Test1

Maybe the function depth was the issue? Moving the code of Test1 into the for loop (All in loop 2), the result was the slowest of all...

So to conclude, I am completely unable to predict when these call optimizations are applied by JS engines.

It's worth noting that browser engines are constantly working on optimizations for common JS patterns. So it often doesn't make sense to optimize your code for their optimizations.


In principle, when you need better performance, avoid function calls, avoid function declarations. But always beware of premature optimization. Imagine how hard it would be to read code without functions!

Upvotes: 0

Jonas Wilms
Jonas Wilms

Reputation: 138247

takeaways:

code is way faster if the engine applies clever optimizations.

Edge is damn slow.

Chrome doesn't hit the fast path in the first case for some reason. Maybe the optimization will only kick in with more iterations.

Wether a function is inside of another function doesn't realy matter², as Firefox proves in this case.

By the way, the best optimization would be:

yup, nothing, as your code has no observable effect whatsoever. Doing nothing can be really fast

²: from a performance perspective not, but from a design perspective it does matter

Upvotes: 0

Related Questions