Skip to content

[vm] Inconsistent inlining due to compilation order dependence #48207

Open

Description

@rakudrama

In the following program, demo1 and demo2 are compiled inconsistently, even though they have the same bodies and are called on the same arguments from the same (indirect) call-site.

main() {
  run(demo1);
  run(demo2);
}

run(void Function(Object?) demo) {
  demo(1);
  demo('2');
  demo(false);
  demo(Duration(seconds: 10));
  demo([]);
}

@pragma('vm:never-inline')
demo1(xx) {
  print(categorize(1));
  print(categorize(xx));
  print(categorize(2));
}

@pragma('vm:never-inline')
demo2(xx) {
  print(categorize(1));
  print(categorize(xx));
  print(categorize(2));
}

categorize(o) {
  if (o is int) return 'int';
  if (o is String) return 'String (length: ${o.length})';
  if (o is bool) return 'bool (value: $o)';
  if (o is Duration) return 'Duration ($o)';
  if (o is List) return 'List (length: ${o.length})';
  return 'other';
}

The output from precompiler2 --trace-inlining supports the following hypothesis:

The first call to categorize is specialized due to a constant argument.
The second call is 'specialized' with no constants and found to be sufficiently large that it is marked as not inlinable.
No further specializations are attempted for constant arguments.

...
Inlining calls in demo2
  Depth 1 ----------
  Polymorphic Instance Calls (0)
  Static Calls (6)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: heuristics (default) with code size:  52, call sites: 8, inlining depth of callee: 0, const args: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: not inlinable
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  Closure Calls (0)
...
Inlining calls in demo1
  Depth 1 ----------
  Polymorphic Instance Calls (0)
  Static Calls (6)
  => categorize (deopt count 0)
     Bailout: not inlinable
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: not inlinable
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: not inlinable
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  Closure Calls (0)
...

The following version is harder to understand.

main() {
  run(demo1);
  run(demo2);
}

run(void Function(Object?) demo) {
  demo(1);
  demo('2');
  demo(false);
  demo(Duration(seconds: 10));
  demo([]);
}

@pragma('vm:never-inline')
demo1(xx) {
  print(categorize(1));
  print(categorize(g++));
  print(categorize(xx));
  print(categorize(2));
}

@pragma('vm:never-inline')
demo2(xx) {
  print(categorize(1));
  print(categorize(g++));
  print(categorize(xx));
  print(categorize(2));
}

int g = 0;

categorize(o) {
  if (o is int) return 'int';
  if (o is String) return 'String (length: ${o.length})';
  if (o is bool) return 'bool (value: $o)';
  if (o is Duration) return 'Duration ($o)';
  if (o is List) return 'List (length: ${o.length}, isEmpty: ${o.isEmpty})';
  return 'other';
}

The first call categorize(g++) is successfully reduced in specialization (on the int type), but the second one is not (has a large size).
The inliner seems to avoid marking the method as not inlinable.

...
Inlining calls in demo2
  Depth 1 ----------
  Polymorphic Instance Calls (0)
  Static Calls (8)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: heuristics (no small leaf)
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  Closure Calls (0)
...
Inlining calls in demo1
  Depth 1 ----------
  Polymorphic Instance Calls (0)
  Static Calls (8)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: early heuristics (default) with code size:  54, call sites: 4, inlining depth of callee: 2, const args: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Bailout: early heuristics (default) with code size:  54, call sites: 4, inlining depth of callee: 2, const args: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  => categorize (deopt count 0)
     Success
       with reason need to count first, code size 1, call sites: 0
  => print (deopt count 0)
     Bailout: heuristics (no small leaf)
  Closure Calls (0)
...

The text inlining depth of callee: 2 looks suspicious.
I suspect the heuristics for inlining not-yet-compiled functions and already-compiled functions are a bit different.

Activity

  1. added
    area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.
    on Jan 25, 2022
  2. marked VM Inlining curiosity #64284 as a duplicate of this issue on Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions