Proposal for @inline(__optimize)

@inline(__always) is being liberally applied across the stdlib out of necessity to achieve some level of performance stability with respect to basic language features. Independently, I'm trying to cleanup the inlining heuristics (without doing a massive overhaul). Even if the situation isn't ideal, I at least want the attributes and heuristics to fundamentally make sense. To that end I need to add an @inline(__optimize) attribute. This will replace most uses of @inline(__always) in the stdlib and will have the intended behavior for the purpose of stdlib performance.

Notes on @inline attributes:

  • Underscored attributes are unsupported. They may disappear or be
    renamed.

  • @inline attributes aren't semantic attributes. They are used for
    manual control of compiler behavior, mostly to bootstrap the
    implementation of language features in the stdlib. In an ideal
    world, they wouldn't be needed.

    ** Always include a comment explaining why the @inline attribute was necessary **

Essentially:

@inline(__always) will do what it says, as originally intended. It will force inlining independent of heuristics. It should very rarely be used in production code, but it is helpful for writing deterministic tests and controlling -Onone performance.

[This is not what @_transparent is for. @_transparent is a form of inlining that happens in the mandatory SIL pipeline and should be strictly limited to supporting diagnostic features. i.e. it is necessary to produce valid SIL. It should never be used for performance reasons.]

@inline(__optimize) will strongly encourage inlining into already optimized code. Philosophically, rather than proving the benefit of inlining, the compiler will need to prove the harm.

Specifically:

  1. @inline(__always) will be inlined at -Onone (this is currently a bug).

    @inline(__optimize) will only be inlined at -O and -Osize.

  2. @inline(__always) will apply to unspecialized generic functions.

    @inline(__optimize) will only be inlined after full specialization.

  3. @inline(__always) bypasses inlining size limits, so can easily cause pathological compile time.

    @inline(__optimize) is subject to code size heuristics and will be
    prevented if the size increase is considered harmful.

Other attributes we could add in the future:

@inline(__early): Inline all of these (along with __always) before other functions to work around some of the problems we have with bottom-up inlining. This could makes sense for certain thunks.

@inline(__late): Don't inline this until "late" inlining. Along with __early, this could help divide stdlib routines into the more/less likely to inline paths.

@inline(__large): Same is @inline(__optimize) but doesn't kick in at -Osize. I'm not in favor of forking -O vs. -Osize, but this could be a useful complement to an @optimize attribute that selectively disables size constraints for certain functions. Hopefully it won't be needed.

10 Likes

Definitely in favor of "__always-means-always", and if that means introducing @inline(__optimize), then sure.


I don't really understand this point, and I think it's worth elaborating on. Especially with opaque values, isn't there still a lot of benefit in inlining one generic function into another?

Alternately, you could just drop this, and leave it as one of the undocumented heuristics the compiler uses for deciding whether to inline an @inline(__optimize) function (or a regular, unannotated function).


I'm against further proliferation of inlining hints (early/late/large) because humans are bad at inlining hints, but I realize that's a philosophical stance that breaks down in practice, and these are all still underscored anyway, meaning they are unsupported for general use at this time.

2 Likes

@jrose, I'm glad you brought this up. The primary motivation for moving a lot of the stdlib functions over to @inline(__optimize) from @inline(__always) is that we're having difficulty controlling code size in the presence of many @inline(__always). We see some significant code bloat when inlining a lot of the generic code before specialization happens on the user side. In the vast majority of cases in the stdlib, we're benefitting from specialization, not the inlining itself. The inlining just provides more opprtunity to devirtualize and expose specialization. So what we really want to say is "please inline this 'fast path' only after the code calling it has been optimized and fully specialized". That will allow the layers of abstraction deeper in the @inlinable call chain to be specialized.

I agree that generic inlining can be useful, and have always argued that we should support it just as a matter of principle. But up to now we've seen more negative than positive impact from it. @Erik_Eckstein can elaborate.

I also don't want to promote the widespread use of attributes to get performance from Swift, but the reality is that you need all of the manual knobs in place just to be able to evaluate the performance impact of individual decisions while you work on automating them. Also, Swift is somewhat unique in the way it implements basic collections with full generality and without using any high level language primitives. A certain amount of shenanigans in the stdlib is pretty unavoidable.

1 Like

All of this sounds reasonable…

…but this doesn't seem like a sensible conclusion. I feel like if the primary concern here is code size, that's covered under "@inline(__optimize) is subject to code size heuristics and will be prevented if the size increase is considered harmful" and shouldn't be called out as a separate rule about generics. For an unlikely example, if the compiler could tell that inlining a generic function could reduce code size, it shouldn't be prohibited from doing so by this pseudo-documented behavior.

I do see that it's a reasonable first pass heuristic to say "inlining generic functions probably increases code size; there's the 'harm' the compiler is supposed to demonstrate".

I agree with that summary. You're right, the primary reason not to use @inline(__always) is simply that code size matters.

I'm still not completely sure where we'll go with generic inlining, but we do want some way to defer inlining some of the deeper generic functions at least until after module serialization. It's tricky because the inliner is bottom-up. Currently people are using @inline(never) to achieve this, but that's definitely not what we want after specialization has reduced the code to something that can be reasonably inlined.

3 Likes