Hacker News new | ask | show | jobs
by olliej 1353 days ago
Hmmm, how did you measure?

This isn't me disagreeing, just me being surprised and trying to think of why the optimizer falls off.

JSC at least has flags on the structure that track which ones will bollocks up caching (e.g. the misery that is looking up things in the prototype chain if the object in question has magic properties that don't influence the structure).

One thought I have is if your test case was something like

   if (a)
     obj.doTheNativeThing()
   else
     obj.doTheOtherNativeThing()
(or whatever)

and you primed the caches by having a being true/false be a 50/50 split, vs all one way. My thinking (I have not done any of the debugging or logging) is that the branch that isn't taken won't insert any information about the call target. I can see that resulting in the generated code in the optimizing layers of the JITs being something along the lines of

    if (a)
       call _actualNativeFunction
    else
       deopt
The deopt terminates the execution flow so then in principle the VM gets to make assumptions about the code state after the whole if/else block, but more importantly the actual size of the code for the function is smaller, and so if you were close to the inlining limit dropping the content of the else branch _could_ result in your test function getting inlined, and then follow on optimizations can happen in the context of the function that you use to run your test with. Even if there aren't magic follow on optimizations removing the intermediate call can itself be a significant perf win.

Testing the performance of engines was super annoying back when I worked on JSC, as you have to try and construct real test cases, but that means competing with your test functions being inlined. JSC (and presumably other engines) have things you can do (outside of the browser context) to explicitly prevent inlining of a function, but then that is also not necessarily realistic. But it's super easy to accidentally make useless test cases, e.g.

    function runTest(f) {
      let start = new Date;
      for (let j = 0; j < 10000; j++)
        f()
      let end = new Date;
      console.log(end - start)
    }

    function test1() {
      ...
    }

    function test2() {
      ...
    }

    runTest(test1)
    runTest(test2)
In the first run with test1, f (in runTest) is obviously monomorphic, so the JIT happily inlines it (for the sake of the example assume both functions are below the max inlining size). The next run with test2 makes f polymorphic so runTest gets recompiled and doesn't inline. Now if test1 and test2 are both small the overhead of the call can dominate the cpu time taken which means that if you simply force no inlining of the function you may no longer be getting any useful information, which is obviously annoying :D