Skip to content

Commit 9d2bf83

Browse files
authored
Inlining: Always inline trivial calls that always shrink (#7669)
Currently a "trivial call" is a function that just calls another function. (func $foo ... (call $target ...)) Where the arguments to `$target` are all flat instructions like `local.get`, or consts. Currently we inline these functions always only when not optimizing for code size. When optimizing for code size, these functions can always be inlined when 1. The arguments to `$target` are all function argument locals. 2. Each local is used once. 3. In the order they appear in `$foo`'s signature. When these hold, inlining `$foo` never increases code size as it doesn't cause introducing more locals (or `drop`s etc.) at the call sites. `$foo` above when these hold looks like this: (func $foo (param $arg1 ...) (param $arg2 ...) (call $target (local.get $arg1) (local.get $arg2))) Update `FunctionInfo` type and `FunctionInfoScanner` to annotate functions with more detailed "trivial call" information that also contains whether inlining shrinks code size. If a function shrinks when inlined always inline it even with `-Os`. Otherwise inline it as before, i.e. when not optimizing for code size.
1 parent b89d65a commit 9d2bf83

File tree

5 files changed

+459
-30
lines changed

5 files changed

+459
-30
lines changed

src/passes/Inlining.cpp

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,32 @@ enum class InliningMode {
6767
SplitPatternB
6868
};
6969

70-
// Useful into on a function, helping us decide if we can inline it
70+
// Whether a function just calls another function in a way that always shrinks
71+
// when the calling function is inlined.
72+
enum class TrivialCall {
73+
// Function does not just call another function, or it may not shrink when
74+
// inlined.
75+
NotTrivial,
76+
77+
// Function just calls another function, with `local.get`s as arguments, and
78+
// with each `local` is used exactly once, and in the order they appear in the
79+
// argument list.
80+
//
81+
// In this case, inlining the function generates smaller code, and it is also
82+
// good for runtime.
83+
Shrinks,
84+
85+
// Function just calls another function, but maybe with constant arguments, or
86+
// maybe some locals are used more than once. In these cases code size does
87+
// not always shrink: at the call sites, omitted locals can create `drop`
88+
// instructions, a local used multiple times can create new locals, and
89+
// encoding of constants may be larger than just a `local.get` with a small
90+
// index. In these cases we still want to inline with `-O3`, but the code size
91+
// may increase when inlined.
92+
MayNotShrink,
93+
};
94+
95+
// Useful info on a function, helping us decide if we can inline it.
7196
struct FunctionInfo {
7297
std::atomic<Index> refs;
7398
Index size;
@@ -77,16 +102,7 @@ struct FunctionInfo {
77102
// Something is used globally if there is a reference to it in a table or
78103
// export etc.
79104
bool usedGlobally;
80-
// We consider a function to be a trivial call if the body is just a call with
81-
// trivial arguments, like this:
82-
//
83-
// (func $forward (param $x) (param $y)
84-
// (call $target (local.get $x) (local.get $y))
85-
// )
86-
//
87-
// Specifically the body must be a call, and the operands to the call must be
88-
// of size 1 (generally, LocalGet or Const).
89-
bool isTrivialCall;
105+
TrivialCall trivialCall;
90106
InliningMode inliningMode;
91107

92108
FunctionInfo() { clear(); }
@@ -98,7 +114,7 @@ struct FunctionInfo {
98114
hasLoops = false;
99115
hasTryDelegate = false;
100116
usedGlobally = false;
101-
isTrivialCall = false;
117+
trivialCall = TrivialCall::NotTrivial;
102118
inliningMode = InliningMode::Unknown;
103119
}
104120

@@ -110,7 +126,7 @@ struct FunctionInfo {
110126
hasLoops = other.hasLoops;
111127
hasTryDelegate = other.hasTryDelegate;
112128
usedGlobally = other.usedGlobally;
113-
isTrivialCall = other.isTrivialCall;
129+
trivialCall = other.trivialCall;
114130
inliningMode = other.inliningMode;
115131
return *this;
116132
}
@@ -132,6 +148,11 @@ struct FunctionInfo {
132148
size <= options.inlining.oneCallerInlineMaxSize) {
133149
return true;
134150
}
151+
// If the function calls another one in a way that always shrinks when
152+
// inlined, inline it in all optimization and shrink modes.
153+
if (trivialCall == TrivialCall::Shrinks) {
154+
return true;
155+
}
135156
// If it's so big that we have no flexible options that could allow it,
136157
// do not inline.
137158
if (size > options.inlining.flexibleInlineMaxSize) {
@@ -143,22 +164,15 @@ struct FunctionInfo {
143164
if (options.shrinkLevel > 0 || options.optimizeLevel < 3) {
144165
return false;
145166
}
146-
if (hasCalls) {
147-
// This has calls. If it is just a trivial call itself then inline, as we
148-
// will save a call that way - basically we skip a trampoline in the
149-
// middle - but if it is something more complex, leave it alone, as we may
150-
// not help much (and with recursion we may end up with a wasteful
151-
// increase in code size).
152-
//
153-
// Note that inlining trivial calls may increase code size, e.g. if they
154-
// use a parameter more than once (forcing us after inlining to save that
155-
// value to a local, etc.), but here we are optimizing for speed and not
156-
// size, so we risk it.
157-
return isTrivialCall;
158-
}
159-
// This doesn't have calls. Inline if loops do not prevent us (normally, a
160-
// loop suggests a lot of work and so inlining is less useful).
161-
return !hasLoops || options.inlining.allowFunctionsWithLoops;
167+
// The function just calls another function, but the code size may increase
168+
// when inlined. We only inline it fully with `-O3`.
169+
if (trivialCall == TrivialCall::MayNotShrink) {
170+
return true;
171+
}
172+
// Trivial calls are already handled. Inline if
173+
// 1. The function doesn't have calls, and
174+
// 2. The function doesn't have loops, or we allow inlining with loops.
175+
return !hasCalls && (!hasLoops || options.inlining.allowFunctionsWithLoops);
162176
}
163177
};
164178

@@ -227,10 +241,35 @@ struct FunctionInfoScanner
227241
info.size = Measurer::measure(curr->body);
228242

229243
if (auto* call = curr->body->dynCast<Call>()) {
244+
// If call arguments are function locals read in order, then the code size
245+
// always shrinks when the call is inlined. Note that we don't allow
246+
// skipping function arguments here, as that can create `drop`
247+
// instructions at the call sites, increasing code size.
248+
bool shrinks = true;
249+
Index nextLocalGetIndex = 0;
250+
for (auto* operand : call->operands) {
251+
if (auto* localGet = operand->dynCast<LocalGet>()) {
252+
if (localGet->index == nextLocalGetIndex) {
253+
nextLocalGetIndex++;
254+
} else {
255+
shrinks = false;
256+
break;
257+
}
258+
} else {
259+
shrinks = false;
260+
break;
261+
}
262+
}
263+
264+
if (shrinks) {
265+
info.trivialCall = TrivialCall::Shrinks;
266+
return;
267+
}
268+
230269
if (info.size == call->operands.size() + 1) {
231270
// This function body is a call with some trivial (size 1) operands like
232271
// LocalGet or Const, so it is a trivial call.
233-
info.isTrivialCall = true;
272+
info.trivialCall = TrivialCall::MayNotShrink;
234273
}
235274
}
236275
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2+
3+
;; With `-O3`, we always inline calls to functions that just call other
4+
;; functions with "trivial" arguments.
5+
;;
6+
;; A trivial argument for now is just an instruction with size 1. E.g.
7+
;; `local.get`, constants.
8+
;;
9+
;; In this test we check inlining when the trivial call arguments are
10+
;; constants. In tests inlining-trivial-calls-{1,2,3}.wast we check locals.
11+
12+
;; RUN: foreach %s %t wasm-opt -all -O3 -S -o - | filecheck %s --check-prefix=O3
13+
;; RUN: foreach %s %t wasm-opt -all -O2 -S -o - | filecheck %s --check-prefix=O2
14+
;; RUN: foreach %s %t wasm-opt -all -Os -S -o - | filecheck %s --check-prefix=Os
15+
16+
(module
17+
;; O3: (type $0 (func (param i32 i32 i32)))
18+
;; O2: (type $1 (func))
19+
20+
;; O2: (type $0 (func (param i32 i32 i32)))
21+
;; Os: (type $1 (func))
22+
23+
;; Os: (type $0 (func (param i32 i32 i32)))
24+
(type $0 (func (param i32 i32 i32)))
25+
26+
;; O3: (type $1 (func))
27+
(type $1 (func))
28+
29+
(type $2 (func))
30+
31+
;; O3: (import "env" "foo" (func $imported-foo (type $0) (param i32 i32 i32)))
32+
;; O2: (import "env" "foo" (func $imported-foo (type $0) (param i32 i32 i32)))
33+
;; Os: (import "env" "foo" (func $imported-foo (type $0) (param i32 i32 i32)))
34+
(import "env" "foo" (func $imported-foo (type $0) (param i32 i32 i32)))
35+
36+
;; O3: (export "main" (func $main))
37+
;; O2: (export "main" (func $main))
38+
;; Os: (export "main" (func $main))
39+
(export "main" (func $main))
40+
41+
;; O2: (func $call-foo (type $1)
42+
;; O2-NEXT: (call $imported-foo
43+
;; O2-NEXT: (i32.const 1)
44+
;; O2-NEXT: (i32.const 2)
45+
;; O2-NEXT: (i32.const 3)
46+
;; O2-NEXT: )
47+
;; O2-NEXT: )
48+
;; Os: (func $call-foo (type $1)
49+
;; Os-NEXT: (call $imported-foo
50+
;; Os-NEXT: (i32.const 1)
51+
;; Os-NEXT: (i32.const 2)
52+
;; Os-NEXT: (i32.const 3)
53+
;; Os-NEXT: )
54+
;; Os-NEXT: )
55+
(func $call-foo (type $1)
56+
(call $imported-foo
57+
(i32.const 1)
58+
(i32.const 2)
59+
(i32.const 3)))
60+
61+
;; O3: (func $main (type $1)
62+
;; O3-NEXT: (call $imported-foo
63+
;; O3-NEXT: (i32.const 1)
64+
;; O3-NEXT: (i32.const 2)
65+
;; O3-NEXT: (i32.const 3)
66+
;; O3-NEXT: )
67+
;; O3-NEXT: (call $imported-foo
68+
;; O3-NEXT: (i32.const 1)
69+
;; O3-NEXT: (i32.const 2)
70+
;; O3-NEXT: (i32.const 3)
71+
;; O3-NEXT: )
72+
;; O3-NEXT: (call $imported-foo
73+
;; O3-NEXT: (i32.const 1)
74+
;; O3-NEXT: (i32.const 2)
75+
;; O3-NEXT: (i32.const 3)
76+
;; O3-NEXT: )
77+
;; O3-NEXT: )
78+
;; O2: (func $main (type $1)
79+
;; O2-NEXT: (call $call-foo)
80+
;; O2-NEXT: (call $call-foo)
81+
;; O2-NEXT: (call $call-foo)
82+
;; O2-NEXT: )
83+
;; Os: (func $main (type $1)
84+
;; Os-NEXT: (call $call-foo)
85+
;; Os-NEXT: (call $call-foo)
86+
;; Os-NEXT: (call $call-foo)
87+
;; Os-NEXT: )
88+
(func $main (type $2)
89+
;; All calls below should be inlined in -O3, but not in -O2 or -Os. We call
90+
;; it multiple times to make sure it won't be inlined because there's only
91+
;; one call, instead it will be inlined based on optimization settings and
92+
;; whether the call is trivial.
93+
(call $call-foo)
94+
(call $call-foo)
95+
(call $call-foo)))

0 commit comments

Comments
 (0)