Repository navigation
[vm] Inconsistent inlining due to compilation order dependence #48207
Description
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