Skip to content

Commit 8149f46

Browse files
committed
Inline always proposal
1 parent df342ae commit 8149f46

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed

proposals/NNNN-inline-always.md

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
# `@inline(always)` attribute
2+
3+
* Proposal: [SE-NNNN](NNNN-inline-always.md)
4+
* Authors: [Arnold Schwaighofer](https://github.com/aschwaighofer)
5+
* Implementation: [swiftlang/swift#84178](https://github.com/swiftlang/swift/pull/84178)
6+
7+
## Introduction
8+
9+
The Swift compiler performs an optimization that expands the body of a function
10+
into the caller called inlining. Inlining exposes the code in the callee to the
11+
code in the caller. After inlining, the Swift compiler has more context to
12+
optimize the code across caller and callee leading to better optimization in
13+
many cases. Inlining can increase code size. To avoid unnecessary code size
14+
increases, the Swift compiler uses heuristics (properties of the code) to
15+
determine whether to perform inlining. Sometimes these heuristics tell the
16+
compiler not to inline a function even though it would be beneficial to do so.
17+
The proposed attribute `@inline(always)` instructs the compiler to always inline
18+
the annotated function into the caller giving the author explicit control over
19+
the optimization.
20+
21+
## Motivation
22+
23+
Inlining a function referenced by a function call enables the optimizer to see
24+
across function call boundaries. This can enable further optimization. The
25+
decision whether to inline a function is driven by compiler heuristics that
26+
depend on the shape of the code and can vary between compiler versions.
27+
28+
In the following example the decision to inline might depend on the number of
29+
instructions in `callee` and on detecting that the call to callee is frequently
30+
executed because it is surrounded by a loop. Inlining this case would be
31+
beneficial because the compiler is able to eliminate a store to a stack slot in
32+
the `caller` after inlining the `callee` because the function's `inout` calling
33+
convention ABI that requires an address no longer applies and further
34+
optimizations are enabled by the caller's function's context.
35+
36+
```swift
37+
func callee(_ result: inout SomeValue, _ cond: Bool) {
38+
result = SomeValue()
39+
if cond {
40+
// many lines of code ...
41+
}
42+
}
43+
44+
func caller() {
45+
var cond: Bool = false
46+
var x : SomeValue = SomeValue()
47+
for i in 0 ..< 1 {
48+
callee(&x, cond)
49+
}
50+
}
51+
52+
func callerAfterInlining(_ cond: Bool {
53+
var x : SomeValue = SomeValue()
54+
var cond: Bool = false
55+
for i in 0 ..< 1 {
56+
// Inlined `callee()`:
57+
// Can keep `SomeValue()` in registers because no longer
58+
// passed as an `inout` argument.
59+
x = SomeValue() // Can hoist `x` out of the loop and perform constant
60+
// propagation.
61+
if cond { // Can remove the code under the conditional because it is
62+
// known not to execute.
63+
// many lines of code ...
64+
}
65+
}
66+
}
67+
```
68+
69+
The heuristic might fail to detect that code is frequently executed (surrounding
70+
loop structures might be several calls up in the call chain) or the number of
71+
instructions in the callee might be to large for the heuristic to decide that
72+
inlining is beneficial.
73+
Heuristics might change between compiler versions either directly or indirectly
74+
because some properties of the internal representation of the optimized code
75+
changes.
76+
To give code authors reliable control over the inlining process we propose to
77+
add an `@inline(always)` function attribute.
78+
79+
This optimization control should instruct the compiler to inline the referenced
80+
function or emit an error when it is not possible to do so.
81+
82+
```swift
83+
@inline(always)
84+
func callee(_ result: inout SomeValue, _ cond: Bool) {
85+
result = SomeValue()
86+
if cond {
87+
// many lines of code ...
88+
}
89+
}
90+
```
91+
92+
## Proposed solution
93+
94+
We desire for the attribute to function as an optimization control. That means
95+
that the proposed `@inline(always)` attribute should emit an error if inlining
96+
cannot be guaranteed in all optimization modes. The value of the function at a
97+
call site can might determined dynamically at runtime. In such cases the
98+
compiler cannot determine a call site which function is applied without doing
99+
global analysis. In these cases we don't guarantee inlining even if the dynamic
100+
value of the applied function was annotated with `@inline(always)`.
101+
We only guarantee inlining if the annotated function is directly referenced and
102+
not derived by some function value computation such as method lookup or function
103+
value (closure) formation and diagnose errors if this guarantee cannot be
104+
upheld.
105+
106+
A sufficiently clever optimizer might be able to derive the dynamic value at the
107+
call site, in such cases the optimizer shall respect the optimization control
108+
and perform inlining.
109+
110+
```swift
111+
protocol SomeProtocol {
112+
func mightBeOverriden()
113+
}
114+
115+
class C : SomeProtocol{
116+
@inline(always)
117+
func mightBeOverriden() {
118+
}
119+
}
120+
121+
@inline(always)
122+
func callee() {
123+
}
124+
125+
func applyFunctionValues(_ funValue: () -> (), c: C, p: SomeProtocol) {
126+
funValue() // function value, not guaranteed
127+
c.mightBeOverriden() // dynamic method lookup, not guaranteed
128+
p.mightBeOverriden() // dynamic method lookup, not guaranteed
129+
callee() // directly referenced, guaranteed
130+
}
131+
132+
func caller() {
133+
applyFunctionValue(callee, C())
134+
}
135+
136+
caller()
137+
```
138+
139+
Code authors shall be able to rely on that if a function is marked with
140+
`@inline(always)` and directly referenced from any context (within or outside of
141+
the defining module) that the function can be inlined or an error is emitted.
142+
143+
144+
## Detailed design
145+
146+
We want to diagnose an error if a directly referenced function is marked with
147+
`@inline(always)` and cannot be inlined. What are the cases where this might not
148+
be possible?
149+
150+
### Interaction with `@inlinable`
151+
152+
Function bodies of functions referenceable outside of the defining module are
153+
only available to the outside module if the definition is marked `@inlinable`.
154+
155+
Therefore, a function marked with `@inline(always)` must be marked `@inlinable`
156+
if it has `open`, `public`, or `package` level access.
157+
158+
```swift
159+
@inline(always) // error: a public function marked @inline(always) must be marked @inlinable
160+
public func callee() {
161+
}
162+
```
163+
164+
### Interaction with `@usableFromInline`
165+
166+
A `public` `@inlinable` function can reference a function with `internal` access
167+
if it is either `@inlinable` (see above) or `@usableFromInline`. `@usableFromInline`
168+
ensures that there is a public entry point to the `internal` level function but
169+
does not ensure that the body of the function is available to external
170+
modules. Therefore, it is an error to combine `@inline(always)` with a
171+
`@usableFromInline` function as we cannot guaranteed that the function can
172+
always be inlined.
173+
174+
```swift
175+
@inline(always) // error: an internal function marked with `@inline(always)` and
176+
`@usableFromInline` could be referenced from an
177+
`@inlinable` function and must be marked inlinable
178+
@usableFromInline
179+
internal func callee() {}
180+
181+
@inlinable
182+
public func caller() {
183+
callee() // could not inline callee into external module
184+
}
185+
```
186+
187+
### Module internal access levels
188+
189+
It is okay to mark `internal`, `private` and `fileprivate` function declarations
190+
with `@inline(always)` in cases other than the ones mention above without the
191+
`@inlinable` attribute as they can only be referenced from within the module.
192+
193+
194+
```swift
195+
public func caller() {
196+
callee()
197+
}
198+
199+
@inline(always) // okay because caller would force either `@inlinable` or
200+
// `@usableFromInline` if it was marked @inlinable itself
201+
internal func callee() {
202+
}
203+
204+
205+
@inline(always) // okay can only referenced from within the module
206+
private func callee2() {
207+
}
208+
```
209+
210+
#### Infinite recursion during inlining
211+
212+
We will diagnose if inlining cannot happen due to calls within a
213+
[strongly connected component](https://en.wikipedia.org/wiki/Strongly_connected_component)
214+
marked with `@inline(always)` as errors.
215+
216+
```swift
217+
@inline(always)
218+
func callee() {
219+
...
220+
if cond2 {
221+
caller()
222+
}
223+
}
224+
225+
@inline(always)
226+
func caller() {
227+
...
228+
if cond {
229+
callee()
230+
}
231+
}
232+
```
233+
234+
### Dynamic function values
235+
236+
As outlined earlier the attribute does not guarantee inlining or diagnose the
237+
failure to inline when the function value is dynamic at a call site: a function
238+
value is applied, or the function value is obtained via class method lookup or
239+
protocol lookup.
240+
241+
```swift
242+
@inline(always)
243+
func callee() {}
244+
func useFunctionValue() {
245+
let f = callee
246+
...
247+
f() // function value use, not guaranteed to be inlined
248+
}
249+
250+
class SomeClass : SomeProto{
251+
@inline(always)
252+
func nonFinalMethod() {}
253+
254+
@inline(always)
255+
func method() {}
256+
}
257+
258+
protocol SomeProto {
259+
func method()
260+
}
261+
262+
263+
func dynamicMethodLookup() {
264+
let c = SomeClass()
265+
...
266+
c.nonFinalMethod() // method lookup, not guaranteed to be inlined
267+
268+
let p: SomeProto = SomeClass()
269+
p.method() // method lookup, not guaranteed to be inlined
270+
}
271+
272+
class A {
273+
func finalInSub() {}
274+
final func finalMethod() {}
275+
}
276+
class B : A {
277+
overrided final func finalInSub() {}
278+
}
279+
280+
func noMethodLookup() {
281+
let a = A()
282+
a.finalMethod() // no method lookup, guaranteed to be inlined
283+
284+
let b = B()
285+
b.finalInSubClass() // no method lookup, guaranteed to be inlined
286+
}
287+
```
288+
289+
290+
## Source compatibility
291+
292+
This proposal is additive. Existing code has not used the attribute. It has no
293+
impact on existing code. Existing references to functions in libraries that are
294+
now marked with `@inline(always)` will continue to compile successfully with the
295+
added effect that functions will get inlined (that could have happened with
296+
changes to inlining heuristic).
297+
298+
## ABI compatibility
299+
300+
The addition of the attribute has no effect on ABI compatibility.
301+
302+
## Implications on adoption
303+
304+
This feature can be freely adopted and un-adopted in source
305+
code with no deployment constraints and without affecting source or ABI
306+
compatibility.
307+
308+
## Future directions
309+
310+
`@inline(always)` can be too restrictive in cases where inlining is only
311+
required within a module. For such cases we can introduce an `@inline(module)`
312+
attribute in the future.
313+
314+
315+
```swift
316+
@inlinable
317+
public caller() {
318+
if coldPath {
319+
callee()
320+
}
321+
}
322+
323+
public otherCaller() {
324+
if hotPath {
325+
callee()
326+
}
327+
}
328+
329+
@inline(module)
330+
@usableFromInline
331+
internal func callee() {
332+
}
333+
```
334+
335+
## Alternatives considered
336+
337+
We could treat `@inline(always)` as an optimization hint that does not need to
338+
be enforced or applied at all optimization levels similar to how the existing
339+
`@inline(__always)` attribute functions and not emit errors if it cannot be
340+
guaranteed to be uphold when the function is directly referenced.
341+
This would deliver less predictable optimization behavior in cases where authors
342+
overlooked requirements for inlining to happen such as not marking a public
343+
function as `@inlinable`.
344+
345+
346+
## Acknowledgments
347+
348+
TODO: ....

0 commit comments

Comments
 (0)