SE-0193 - Cross-module inlining and specialization

In a sense this is by design -- these are low-level attributes that are intended for use in very narrow situations. Someone shipping a resilient binary framework in Swift has to be aware of a fair amount of implementation detail, and having attributes map directly to the implementation model is actually an advantage.

1 Like

This is quite different from public private(set). The latter does not expose the private setter in the ABI; a public private(set) property is indistinguishable from a public read-only property to the user.

1 Like

I haven't read the post in detail, but declaring the sync() function as @inlinable should allow the optimizer to eliminate the runtime generics and closure allocation, so perhaps that will solve some of the performance problems that author was seeing.

1 Like

The parser can implement arbitrarily-complex grammar for attributes, as you can see with the more complex ones such as @available. However I wouldn't worry too much about @inline(never), since it's set of valid use-cases is even narrower than @inlinable. In fact I would not expect to see it used anywhere except to work around optimizer bugs, or for benchmark purposes.

is inlineable really the right term though? doesn’t it just mean that the function is emitted into the client without actually getting inlined into a larger function. like couldn’t it get specialized in the client but still stay a function that gets called within the client

I worry more about @inline(__always) being an attractive nuisance: reading as having less qualifiers than @inlinable, a false sense of absoluteness for someone with no prior knowledge of it’s lack of cross-module applicability.

Actually @inline(__always) is implemented as having the same restrictions as @inlinable (body can only reference public symbols). It is not an official part of the language and is part of this proposal though. In particular in the current implementation it doesn't actually guarantee inlining will take place (there are some restrictions around generics, and also nothing will happen in -Onone mode).

That makes sense. I think @export instead of @inlinable communicates better what is happening and prevents confusion with @inline(...).

Even though inlining is the primary use case, 'inlinable' doesn't communicate the effect it has on your code and could be confused with inlining within the module. @export tells you very clearly that your code will be exported outside of the module.

The proposal actually uses the same wording:

Edit: I guess @exported instead of @export might be better, since all Swift attributes are descriptive. I found ‘export’ nicer, probably because it’s shorter, but I can get used to ‘exported’.

As the review manager, I've intentionally withheld my own commentary in this review thread. This is a custom for the review manager so that they do not give the appearance of imposing their own bias on faithfully representing the review discussion with the Core Team. However, I'd like to wade into the discussion with some perspective which — although captures some of my personal opinions of the topic at hand — hopefully can help frame some of the items being considered here.

There are two important technical points I wanted to underscore:

  • An API that is marked @inlinable can — at the compiler's choice — have its implementation body inlined on a case-by-case basis into the client code. API marked @inlinable will still have a non-inlined implementation provided in the binary corresponding to the module that publishes the API. A consequence is that essentially at runtime we can have multiple instances of an APIs implementation within a runtime process: the one in the binary (such as a framework) that publishes the API and the other ones inlined into client code. If you take this a step further and consider the case of a newer version of a binary framework (that published the API) being used than the one a client was built against, then the inlined version of the API burned into the compiled client code may be an older — and different — version of the API provided inside the binary framework. Thus an API marked as @inlinable needs to be well-considered in that beyond just being "exposed", "exported", or "published" that it needs to contend with the fact that multiple versions of that API's implementation might exist — and need to co-exist — at runtime. More broadly, this means that details of the implementation of an @inlinable API now become a binary compatibility concern beyond just looking at the normal separation-of-concerns we normally think of for API contracts at API boundaries.

  • Similarly, an API marked @abiPublic — which is intended to only allow APIs marked @abiPublic to access some otherwise non-public symbol — needs to take into account these implications. They essentially become semi-published API, with binary compatibility concerns on previous versions of the @inlinable APIs that call them.

If we take these two points into account, for me spellings like @export, @exported or @exposed as being alternative spellings for @inlinable and @abiPublic do not adequately convey the nuanced semantic implications I've outlined above that are crucial to consider when using the @inlinable or @abiPublic attributes. Given the aforementioned subtleties of using these attributes, especially on APIs published by binary frameworks, the intent is that the attributes @inlinable and @abiPublic (however they are ultimately spelled) will be used (a) infrequently and (b) in well-considered cases as they represent a special kind of permanent commitment with regards to the contracts they place on a published API.

This is why the spelling of the attributes — at least as motivated by the proposal — are intended to be literal and technical. As this review has shown, it is challenging to find the spellings of these attributes that balance the concerns I mention while not sounding too broad or vague. They are intentionally not meant to be "expressive in your code" (as @Karl suggested) because they really are expressing very low-level implications about what the compiler does with the implementation of an API which carry some nontrivial implications on the API going forward. In other words, they are meant to be power tools only to be used by those who fully understand the implications of using them.

6 Likes

The technical points you mention are exactly why I think @exported is a good alternative for @inlinable. It tells you that the body is exported out of your module and thus makes you think about the implications for binary compatibility. @inlinable has the same effect, but it is a bit confusing with @inline(...), and since @inlinable does not mean that it will actually be inlined it seems to me that @exported would be more clear, more technically correct and doesn't give the impression that it will always be inlined.

Side note: @exported could perhaps also be used on non-computed static let properties.

Regarding @exposed instead of @abiPublic. Again, in my view this tells you that your function is exposed to the outside world and makes you think about the implications. I've also seen the suggestion @linkable, which seems like a good candidate as well to me.

To be honest, I actually don't have a problem with @abiPublic, so if that it is the best we can come up with and we don't mind about the term ABI being foreign to some people, I'd say just go with that. As you say, these are power tools, so I don't see a problem here. What does seem wrong to me though is unnecessarily connecting this attribute to inlining by using something like @inlineAvailable or @availableFromInlinable.

I definitely respect that line of reasoning.

For me personally (as just another reviewer of the proposal and not the review manager) @exported reads more like a synonym for public than telling me anything about the body of the implementation being exported out of the module. It also doesn't strike me as being obvious that an API being "exported out of your module" means it now available for inlining — and all the semantic implications that result from that. In this case, I feel like one just has to know that "exported" is referring to the API's implementation body, and know what the compiler will end up doing with that implementation body, to understand the ramifications of applying the attribute to an API.

I see the reasoning here. Similar to my concerns about @exported, however, "exposed to the outside world" also feels like a stretch to me as the connotations of the term "exposed" aren't readily clear. For someone who is justing seeing this attribute for the first time, in what way is the API "exposed"? It's quite subtle — it means an otherwise non-public API can be linked to from another binary due to inlining of another API that calls it. In many ways it is quite a low-level concept.

That said, users can learn what these attributes mean, and I definitely can see the reasoning behind the spellings you suggest.

1 Like

I agree that the suggested attributes are less familiar than @inlinable. I'm indeed assuming that people will learn them. In one final attempt to make @exported a bit more clear I'd suggest @exportedBody. However, unless others think it would be good to go into that direction I won't pursue this any further since @inlinable was already accepted and this extended review was meant to be dealing with alternatives for @abiPublic.

Regarding @abiPublic, if a more familiar technical term is preferred, the more I think about it @linkable seems to be a very good fit.

The way I arrived at this idea was that these keywords are all about versioning, but I might not even know for sure which version of the module I'm currently looking at (maybe I'm switching branches a lot, trying to track down a bug), or how it looked at any arbitrary version. So really, the thing that's most important to me is: what am I not allowed to change? - or, in other words, how strongly have I committed to using this API, even internally?

The specific optimisations that are possible as a result of those commitments are secondary, and which ones the compiler actually decides to use are another matter entirely.

So... I mean, I'm aware that it's a non-conventional way of looking at things, but I don't think it's foolish. It has the benefit of letting us collapse type-specific keywords in to fewer, more-abstract concepts with type-specific considerations.

But it's fine; we should just be consistent. And it is a pro-feature (although that doesn't mean non-pros won't encounter it).

I see. That's the motivation for why you suggested @frozen earlier in the thread. I also see some affinity with the suggestion to use @exported or @exposed.

To be clear, I was not trying to characterize this suggestion as foolish. I think it somewhat boils down to the view point we want to most emphasize the most on the use of these attributes. I think I have a better appreciation now of what you were advocating. Thank you.

1 Like

I'm with Ted on that point. 'exported' is commonly used to specify that a symbol will be visible outside of a 'module'. It does not convey any meaning about the body of the function.

This is especially confusing with a language where there is not header. In C, and C++, you can still say that a function exported in a header is assumed to be inlinable, but in swift, there is not such hint.

Using exported to tell the body may be copied outside of the module would be a source of confusion and errors for anybody coming from an other language.

If this is a concern, then maybe combine the two into @abiExposed? The primary concern with rare technical attribute keywords like we are talking about is that a programmer coming upon them for the first time doesn't make a wrong assumption about what it means. Therefore the attribute name should:

  1. Not seem like it "obviously" means something else (e.g. I continue to argue against including "Public" as part of the spelling to keep first-time readers from thinking it is somehow synonymous with "public").

  2. Have a somewhat unique spelling in our domain so that someone googling it will quickly find an explanation. (I am a bit disappointed to find that there is an @expose attribute that means disparate things in Java and Python, neither of which are this meaning -- though Python is in the ballpark. At least there is no @exposed.)

The other thing is that there are actually a whole spectrum of potential ABI commitments, ranging from simply "visible" (public/@abiPublic) to "you can, essentially or literally, copy the declaration in to your client app" (inlineable/frozen/fixed_contents). We've focussed on these two extreme commitments, but there are lots of steps in-between:

  • A function might want to guarantee that it is "pure", so there is more freedom to rearrange it when compiling the client app, without going full-hog @inlineable. There has been talk about this attribute before, and within a single module it theoretically isn't even necessary, but it could allow interesting things across module boundaries.
  • A type might want to guarantee that it will have a certain maximum size (e.g. always fits in 2 pointers), or is always a reference-counted struct/enum, without exposing the specific members that make it up (e.g because it is an enum with private cases, or because the author doesn't want to add @fixed_contents).

So this is why I think it's better to have a parameterised attribute - it allows us to scale and experiment with other kinds of optimisations while avoiding keyword explosion. Nobody wants a @maximumSize or @isRefCounted attribute - something like @frozen(maximumSize: .pointerSize(2), containsRefCountedMembers) would be way easier to understand.

Are we likely to experiment with such optimisations in the future?

I previously suggested it'd make more sense to have @hidden public rather than ABI public, but I'd like to revise that position. Whereas public and internal are about deciding what is visible to external users of the library, after reading the rest of the discussion I now think it makes sense for internal implementation details of @inlinable not be described the same way.

Another question I'm wondering about is this: I can easily imagine a small @inlinable function wanting to call another small function expecting that the second function will be inlined too. Can this second function be both @inlinable and internal? And if so, should it be marked @abiPublic too? That's a bit confusing.

I'm trying to put some order into all this. I think we have two distinct concept at play:

  1. visibility: what parts are visible to the user of the library (API)
  2. exposure: what is visible to the compiler/linker when using the library (ABI)

@inlinable and @abiPublic are two modes of exposure. One exposes both the body of the function for inlining in addition to a symbol that can be called, while the second only exposes the symbol. Neither change what is visible to a user of a library at the API level however.

Once you see things this way, I think it makes sense to express the two exposure modes under the same base name:

@exposed(inlinable)
@exposed(callable)

In other words, exposure would be on a different axis than visibility and its modes would be inlinable, callable, and no exposure. Its relation to visibility would be:

  • public implies @exposed(callable), but can be upgraded to @exposed(inlinable)
  • internal implies no exposure, but can be upgraded to either @exposed(callable) or @exposed(inlinable).

Alternatively, we could make exposure modes not take a parenthesis:

@exposedInlinable
@exposedCallable

which could be more friendly for mixing with @available at a later point.

I don't like 'callable'. It suggests that the user can call it directly, which is not the case.

As Ted mention, I think this is not an issue if the term are technicals and somewhat cryptic for non advanced users. @exposed(linkable) would explicitly tell this is an annotation for the compiler and not for the user.

That would work too.