javascriptinlinev8jit

js v8 function inlining


I'm intresed how and in what cases V8 perform inlining. I know that V8 can inline functions to eliminate call cost, but i dont konw the rules for it to happen. I got how it works in obvious cases, like this:

// object with persistent shape
class Obj {
    prop = 42
}
// function always called with persistent types/shapes of args
function obj_accessor(obj) {
    return obj.prop;
}

function main() {
    const obj = new Obj();
    // this call is highly likely to be inlined
    const val = obj_accessor(obj);
    console.log(val);
}

but if i use higher order functions, in what cases it will perform inlining, if in any?

I have some examples

class Foo {
    bar = 42;
}

class Bar {
    foo = 42;
}

function foo_accessor(obj) {
    return obj.bar;
}
function bar_accessor(obj) {
    return obj.foo;
}

function main(obj, accessor) {
    // will inlining be performed at all ?
    // is it per call site or per function
    // so after passing more then single accessor it
    // can not be optimized any more ?
    const val = accessor(obj);
    console.log(val)
}

// accessor not statically know -> no inlining ? 
main(...(Math.random() < 0.5 ? [new Foo(), foo_accessor] : [new Bar(), bar_accessor])); 

// accessor statically know -> it can be inlined
main(new Foo(), foo_accessor);

// accessor statically know -> it can be inlined
main(new Bar(), bar_accessor);

as mentioned in comments every function have single feedback vector, so i can assume, that inlining share similar behaviour with inline cache, and only way to higher order function to be inlined is consistently pass same instance until v8 optimize function call


Solution

  • The general answer is: no, V8 does not inline polymorphic indirect function calls (where "indirect function calls" is what you call "higher order functions", and is also often called "callbacks"). There are exceptions to this, for instance because of constant folding.

    There is an opened feature request for this on the V8 bug tracker (Support polymorphic inlining for regular function calls), which contains a few explanations.

    Your example can be simplified to:

    function bar1() { return 42; }
    function bar2() { return 17; }
    
    function foo(fn) { return fn(); }
    
    print(foo(bar1));
    //print(foo(bar2));
    

    If you only call foo with bar1 as input, then V8 will be able to inline bar1 in foo. However, if you also call foo with bar2 as input, then the call fn() will not be inlined at all.

    Technical details: The way V8 inlines bar1 in the first case is because when it executes foo in the interpreter (= before optimizing the function), it gathers feedback (which used to be inline caches) which records that this call to fn has always been calling bar1 so far (in technical V8 terms, the feedback is monomorphic). Then, when Turbofan or Maglev optimize foo, they will look at the feedback vector and see that so far, only bar1 was called, and they will speculate that in the future, only bar1 will be called. If you eventually pass anything other than bar1 to foo, then the function will deoptimize.
    However, if both bar1 and bar2 have been seen for fn, then the feedback will not record anything regarding the specific functions that have been seen for fn. Thus, when Turbofan optimizes foo, it cannot speculate on anything regarding fn, and thus doesn't inline anything.
    Not that for specific constructs (like property loads), the V8 feedback also has a "polymorphic" state where it records that multiple different objects have been seen so far. V8 doesn't do this for calls.

    I mentioned earlier that constant folding can allow to inline polymorphic function calls. So consider this example instead:

    function bar1() { return 42; }
    function bar2() { return 17; }
    
    function foo(fn) { return fn(); }
    
    function main() {
      return foo(bar1) + foo(bar2);
    }
    

    Here, the call to fn in foo is still polymorphic, but when main gets optimized and the foo(bar1) and foo(bar2) calls get inlined, then fn will refer to the constant bar1 and bar2 values respectively, and thus the fn() calls can be inlined (even if the feedback in foo was polymorphic) (and constant-folding will further optimize the whole main function to just do return 59). The example in your question might fall in that category, but it's hard to tell for sure without more details: is your whole code inside a function or at the top level? Has feedback been collected for all calls to main? etc. etc.