-
Notifications
You must be signed in to change notification settings - Fork 2.5k
@inline(always)
proposal
#2958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
@inline(always)
proposal
#2958
Conversation
public'ish declarations
ensures that there is a public entry point to the `internal` level function but | ||
does not ensure that the body of the function is available to external | ||
modules. Therefore, it is an error to combine `@inline(always)` with a | ||
`@usableFromInline` function as we cannot guaranteed that the function can |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`@usableFromInline` function as we cannot guaranteed that the function can | |
`@usableFromInline` function as we cannot guarantee that the function can |
@inline(always) | ||
func mightBeOverriden() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there ever a situation where a non-final class method would be able to be guaranteed inline? Should this be diagnosed as an error at the declaration site?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there ever a situation where a non-final class method would be able to be guaranteed inline?
In theory, yes. But we would be relying on "mandatory optimizations" doing function local type propagation and devirtualization.
func localDevirt() {
let c = BaseClass()
c.method() // c's type is know to be BaseClass therefore we can replace it by a "direct reference"
}
But, I think we should weight this against the intention that we want the method to be inlined in "most cases". Therefore, I think making a non-final method with @inline(always)
an error is more in spirit with the desire for @inline(always)
to be an optimization control.
Should this be diagnosed as an error at the declaration site?
Yes. Thank you for pointing this out. Thinking more about the intend of the attribute optimization control, it should be an error marking a non-final class method as @inline(always)
because there is no way of "directly calling it" without semantically having to go through a v-table.
This is in contrast to the call to an enum/struct's method which can be called directly (via the struct/enum's type) or through a protocol conformance.
This proposals takes the position to give `@inline(always)` the semantics of | ||
`@inlineable` and provide an alternative spelling for the case when we desire | ||
`@_alwaysEmitIntoClient` semantics: `@inline(only)`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Language Steering Group discussed this and we feel that the @inlinable
semantics are likely the right choice for @inline(always)
, but that @_alwaysEmitIntoClient
is more of an orthogonal concept, since it just means that the body will be emitted into the client object but not necessarily inlined at the call sites (even though the optimizer may choose to do so since it can see the body). So we're requesting that this form of the attribute be removed from the pitch.
See also Doug's comment in the thread: https://forums.swift.org/t/pitch-inline-always-attribute/82040/22
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay.
As outlined earlier the attribute does not guarantee inlining or diagnose the | ||
failure to inline when the function value is dynamic at a call site: a function | ||
value is applied, or the function value is obtained via class method lookup or | ||
protocol lookup. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This carve-out seems like it defeats some of the goals of the guarantee and associated diagnostic, since the proposal says in one place that we should emit a diagnostic when it's not guaranteed, but then it lists these exceptions. I'm trying to think of concrete cases that are left where we can decide that inlining isn't possible and would diagnose it.
The recursion case you mention above is one where inlining would definitely not be possible.
The protocol/concrete type case is one where it makes sense to not diagnose. For example,
protocol P { func someFunc() }
struct S: P {
@inline(always) func someFunc() {}
}
In this case, if someFunc
is called on an instance of the concrete type, we know we can inline that, but if it's called on a generic or existential constrained to P
, we couldn't. That seems fine, and I don't think it makes sense to diagnose in that case (because from the usage site, we don't even know about S
). Do you agree?
I mentioned the non-final class method situation in another comment. Should it always be an error to declare one of those as @inline(always)
?
In general, I think there are up to four situations we need to consider when something is marked @inline(always)
:
- The function can definitely be inlined at the usage site.
- The function can never be inlined and the compiler should diagnose an error to try to declare it that way.
- The function can sometimes be inlined but not for some specific usage site and the compiler should diagnose an error at the usage site.
- The function can sometimes be inlined but not for some specific usage site and the compiler should not diagnose an error at the usage site.
I'm not sure whether there are any cases of (3) that exist, though.
I think the proposal would benefit if we could clearly show examples of each of the situations above. My reading of it currently is that the examples that just say "not guaranteed to be inlined" aren't always clear because the proposal says that in some situations those are diagnosed and in others they aren't. Can we see explicitly the situations when they are and aren't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This carve-out seems like it defeats some of the goals of the guarantee and associated diagnostic, since the proposal says in one place that we should emit a diagnostic when it's not guaranteed, but then it lists these exceptions. I'm trying to think of concrete cases that are left where we can decide that inlining isn't possible and would diagnose it.
I will list the cases as requested. In the initial version of the proposal I required explicitly spelling out @inlinable
if @inline(always)
was requested on public'ish (public, package, open) declarations, so the set of diagnostics was bigger. I was convinced by feedback on the pitch that we should imply @inlinable
instead of being explicit about it.
Yes, the guarantee is mostly upheld by declaration side "measures" (implying @inlinability or explicitly spelling it out for public declarations).
The recursion case, because the cycle can be introduced by another function, is the only instance that can be viewed as sort of a "usage side" diagnostic (3. in the list above).
The protocol/concrete type case is one where it makes sense to not diagnose. For example,
protocol P { func someFunc() } struct S: P { @inline(always) func someFunc() {} }
In this case, if someFunc is called on an instance of the concrete type, we know we can inline that, but if it's called on a generic or existential constrained to P, we couldn't. That seems fine, and I don't think it makes sense to diagnose in that case (because from the usage site, we don't even know about S). Do you agree?
Yes. That was what I meant to capture in the sentence:
As outlined earlier the attribute does not guarantee inlining or diagnose the
failure to inline when the function value is dynamic at a call site: a function
value is applied, or the function value is obtained via class method lookup or
protocol lookup.
I agree, with both of your comments that I should change the proposal to make non-final class method annotation an error; and that I should add the protocol call with a struct's conformance as an example that we don't diagnose and don't guaranteed.
I mentioned the non-final class method situation in another comment. Should it always be an error to declare one of those as @inline(always)?
Yes. I think that is more in spirit with the intention of @inline(always)
being an optimization control. There is no way of directly calling the function (unlike for methods of struct
/enum
's), semantically it has to go through dispatch so we can't guarantee it. Therefore, I agree with the position we should be flagging this as an error.
I think the proposal would benefit if we could clearly show examples of each of the situations above. My reading of it currently is that the examples that just say "not guaranteed to be inlined" aren't always clear because the proposal says that in some situations those are diagnosed and in others they aren't. Can we see explicitly the situations when they are and aren't?
Yes.
A sufficiently clever optimizer might be able to derive the dynamic value at the | ||
call site, in such cases the optimizer shall respect the optimization control | ||
and perform inlining. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate more on cases where this happens in practice, if any?
For example, if we consider the following code:
@inline(always)
func binaryOp<T>(_ left: T, _ right: T, _ op: (T, T) -> T) -> T {
op(left, right)
}
@inline(always)
func add(_ left: Int, _ right: Int) -> Int { left + right }
_ = binaryOp(5, 10, add) // (1)
_ = binaryOp(5, 10) { add($0, $1) } // (2)
Could I expect the Swift optimizer today to fully inline (1), (2), both, or neither?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The paragraph meant to express that there are cases beyond the case we guarantee where the Swift optimizer in say optimizing mode (e.g -O
or -Osize
) is more aggressive and will (should) take the @inline(always)
into account as part of its heuristics.
The cases you mention are optimized today at -O(size)
but are not optimized in -Onone
and should not be expected to.
I will incorporate this example to make this more clear.
We only guarantee inlining if the annotated function is directly referenced and | ||
not derived by some function value computation such as method lookup or function | ||
value (closure) formation and diagnose errors if this guarantee cannot be | ||
upheld. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed below, I think this paragraph/section might benefit from a bit of wording, since this seems to imply that we'll definitely diagnose cases where it isn't guaranteed but then later we discuss some situations where it's not diagnosed. I think the word "guarantee" here is a bit confusing since it seems to imply both situations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Re-reading this paragraph it is very misleading. I will rework it.
No description provided.