Search code examples
javascriptperformancefunctionv8

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


Solution

  • 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.