When should both @inlinable and @inline(__always) be used?

Looking at the Standard Library implementation, I can see (short method) examples of both:

extension Foo {
  @inlinable
  func bar() -> Baz { ... }
}

and

extension Foo {
  @inlinable
  @inline(__always)
  func bar() -> Baz { ... }
}

min and max are examples of the former and ClosedRange's clamped is an example of the latter.


I remember isolating a case in some (of my own) performance sensitive code where replacing
a = min(b, c)
with
a = b < c ? b : c
resulted in more optimized generated code (this only happened within certain contexts, so it is not generally the case), could this have been caused by the fact that min is not annotated with @inline(_always)?

7 Likes

@inlinable is really about visibility--it makes the function body part of the the Swift interface for the module, rather than in internal detail. Quoting the introduction of SE-0193:

The @inlinable attribute exports the body of a function as part of a module's interface, making it available to the optimizer when referenced from other modules.

@inline(__always) is a compiler directive that does not change function visibility at all; it simply tells the compiler to ignore inlining heuristics and alwaysÂą inline the function. In particular, note that a function that is @inline(__always), but not @inlinable, will not be available for inlining outside of its module, because the function body that would be inlined is not available.

It's unfortunate that Swift inherited the C and C++ legacy of conflating these two orthogonal semantic notions (whether or not a function body is part of the API vs whether or not a function call should be generated) under the single term "inline", but, well, it did.

Note that adding or removing @inlinable changes the API of a module. Adding or removing @inline(__always) does not.

@inline(__always) can be beneficial for performance (especially in micro-benchmarks), but it can also have catastrophic downstream effects in macro performance due to code-size increase. We may need finer-grained semantics to be available (e.g. "always inline this when monomorphized, otherwise never") in order to reason about this, or we may simply need better heuristics for the compiler, or both, but @Michael_Ilseman has found some cases where aggressive use of @inline(__always) was killing performance due to code size blowup, so we definitely don't want to simply stamp it on everything. @Andrew_Trick has sketched out proposals for finer-grained attributes in the past, and I hope that he'll get some time to work on it more.

Âą well, mostly. There are some weird corner-cases here, which Andy has also discussed fixing, but this is a good enough definition for the purposes of this comment.

40 Likes

Maybe Swift should also add __attribute__((flatten)) equivalent @inline(flatten) to inline directive family to make a balance between pros and cons of nested/recursive inline problem.

My mental model when working on the stdlib is: @inline(__always) is a blunt instrument for working around the compiler, but relatively harmless. Verify generated assembly before and after but it doesn’t impose irrevocable consequences. inlinable means “make my life immensely harder in the indefinite future in exchange for wins now”, and should be avoided if at all possible. Sometimes you need it to get reasonable performance, but it’s very scary.

Note that this is only true for authors of libraries with stable public API/ABI. If you aren’t a library author or are fine with recompiling library clients when you change things, there’s little to no danger from inlinable.

9 Likes

Both you and @scanon said that @inlinable is a difficulty for authors of libraries with a stable public API or ABI, but so far as I know that’s not true. @inlinable has no API stability effect that I am aware of. It exposes nothing new in the API. It exposes new things in the generated interface, but all access modifiers are still enforced.

@inlinable is only troublesome for those with stable ABIs. For those with stable APIs only (i.e. SwiftPM packages), it is mostly an annoyance (gotta sprinkle @usableFromInline everywhere) but imposes no other costs.

While we’re here, one of the biggest problems with @inlinable is that there is no good heuristic on where to apply it. Cross-module optimisation mode applies the first-order heuristic, which is to apply it to all generic functions. This is a good starting point: without @inlinable all calls to generics in other modules will be made to the unspecialised generic, which will often be vastly slower than calls to the specialised one. Using @inlinable enables the compiler to specialise the generic function. Note that, contrary to @inlinable’s name, the compiler may specialise but not inline that function.

The more subtle problem is that there are a bunch of smaller cases where adding @inlinable can lead to meaningful performance boosts in non generic contexts. For example, I’ve seen cases where adding @inlinable removed a refcount operation and so elided a CoW. Another example is with Collections, where without the @inlinable the compiler cannot see the implementation of index(after:). If this implementation is straightforward enough, the compiler can greatly simplify loop code by being able to inline the implementation.

So @David_Smith I think your mental model is a bit off. If you’re compiling in library evolution mode then yes, @inlinable needs to be handled with extreme care. If you aren’t, @inlinable is safe and effective.

7 Likes

Actually, let’s be clear for other readers. When we say “@inlinable is dangerous if you have a stable ABI”, what do we mean?

@inlinable has two effects. Firstly, it makes the implementation of this method public and able to be inlined into the caller. Secondly, it forces you to make everything it calls @usableFromInline. Each of these has an ABI effect.

Working backwards: every symbol you tag with @usableFromInline is now part of your public ABI. For ABI stability purposes it is as though this method is public. You can only evolve it in ways that are compatible with being a public function (which is to say, you cannot really evolve its signature, but you can change its implementation). This is obviously a big burden, and it may also be a surprise to some folks.

However, the method body being public is a bigger deal. Because callers are allowed to inline the method body, you have to assume that any future user of your library will not use the code you shipped in your library, but will instead use the first-ever version that they inlined. This means that, if there are bugs in that function, they are there forever. If that function needs to call different interfaces: tough, the old interfaces must continue to work. Essentially, you need to be very confident that this function isn’t going to need to be totally rewritten, because you can’t do that anymore.

In library evolution mode, @inlinable methods are standing in a hall of mirrors. They can be changed, but the previous versions of them live on in callers, and so the previous versions must still work. This means, in practice, only trivial changes are acceptable. This is a huge burden.

12 Likes

I mean I did say I was talking about my model for working on the stdlib, which does have library evolution turned on. But yes, good clarification.

The OP is asking about the attributes in the context of the standard library, so we’re in this case.

1 Like

This can be cleared up once and for all by introducing attribute aliases:
@export(implementation) -> @inlinable
@export(interface) -> @usableFromInline
And cleaning up the stdlib to use clear names.

"Export" communicates unambiguously that we're only talking about ABI-level semantics. It has no affect on semantics of visibility at the type system level, and only incidentally affects inlining at the level of performance. In fact, @inlinable @inline(never) is one of the key attribute combinations for code that needs to be specialized, but inlining it will explode the code size to no real benefit.

5 Likes

It's got "-able" right there in the name! :-)

(The name of "inlinable" was discussed heavily. At the time it was considered the least bad option, since it got the most important point across concisely: "body available to clients".)

1 Like

I don’t have a better idea for a name, I just wanted to note that this behaviour is not always obvious to people. I’ve seen folks be confused about the combination of @inlinable @inline(never) when I’ve suggested it in the past.

2 Likes

I understand the logic behind this, yet it strikes me as extremely confusing, since @inlinable exports the implementation of the function while @usableFromInline makes it available in the module’s interface.

2 Likes

The other way around, right?

  • @inlinable makes the implementation itself ABI so should be @exported(implementation)
  • @usableFromInline makes the function's interface (but not its implementation) ABI so should be @exported(interface)

It would also be good if we could find a better spelling for @inline(never) @inlinable which means it must not actually inline the body but may still create a specialised version of this. We use @inline(never) for a bunch of slow-paths in NIO. But some slow-paths are suuper slow without specialisation so we do use @inlinable @inline(never) which always requires a comment because it sounds so wrong :slight_smile:.

4 Likes

@exported(implementation) @inline(never) seems acceptable to me for this purpose.

1 Like

Actually, that's not bad, yes!

Yeah, it's confusing when I paste things on the wrong line. I edited my post.

4 Likes

If @inlinable and @usableFromInline is about what is accessible outside the module, maybe it should parallel other access keywords more? It would also make more clear that everything can be inlined and used from inline from inside the module.

public(implementation) -> @inlinable
public(abi) -> @usableFromInline

1 Like

The core team wanted to avoid the term "ABI." We've already been through this naming exercise; you can see why these were not the chosen spelling in the previous threads. There is no need to revisit here.

1 Like

It's worth noting that we want to refine these attributes anyway in order to add availability information (i.e. express things like "this method is inlinable from macOS 10.15 on; on other OSes it should be emitted into client"). In light of that requirement and the ambiguity discussed here, a careful rethinking is needed.

See Chris's post on the original SE-0193 thread for some discussion of this as well.

1 Like

@export(implementation) and @export(interface) are names I have always liked and I think makes things clearer!

The only caveat right now from me is I am worried slightly that people may think that one needs to export both the implementation /and/ the interface. Maybe I am worried over nothing. That being said, if we do think it is an issue, I think we could work around it by using instead declaration and definition. E.x.:

@export(definition) and @export(declaration).

I think this may avoid such confusion since then it is sort of obvious that exporting a definition must also export its declaration. But again, I am just thinking out loud and this is obvious.

2 Likes