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

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

@export is potentially confusing because of collision with the similar notion of "exporting symbols" in C-family compilers. I don't think that's a dealbreaker, but it might be a weak point in favor of something else (and Swift already uses "public" for this notion, mostly).

1 Like

Perhaps:

@exposed(implementation)
@exposed(interface)
2 Likes

Wait, I'm only bringing this up so we don't need to keep repeating this naming exercise for everyone who encounters this feature. I'm not interested in re-bikeshedding names. I'm speaking up on behalf of people who keep coming to me confused. The problem won't go away until the name is changed.

At the time this was discussed on the forum, I did not understand the core team logic. It was only much later that I realized it was a deliberate attempt to hide the functionality of the attribute. So there was a basic problem with how the name was chosen. This is also the reason it took so long to pick a name.

The only way to understand the semantics of library evolution is to understand that functionality: when the compiler produces a binary, it can bake-in assumptions about anything that was exported from other modules, and that binary doesn't need to be recompiled when the exported things change.

If we introduce a feature that controls the semantics of library evolution, then the name needs to be specific to library evolution. The obvious way to do that is to evoke the functionality of the compiler: it exports information that gets baked into other binaries.

Bikeshedding aside, whatever name you pick should not be related to access modifiers, public/protected/private. They exist in a separate domain of semantics. Naturally, higher-level semantic domains imply things about lower-level domains. So, of course a public symbol's interface needs to be exported.

The important thing is that we first know what domain we're talking about.

For the same reason, the name can't have anything to do with inlining, specialization, or anything else in the performance domain. After all, a function certainly does not need the @inlinable attribute to be inlinable! Inlining by definition does not affect program semantics outside the performance domain.

Obviously, functions in a library built with library evolution enabled can't be inlined or specialized if they aren't exported, but that's not the crux of the issue. The compiler can't make any assumptions about those implementations unless that information is explicitily exported!

2 Likes

We use import in a file to mean that we want to use another module's API within that file. Meanwhile, the cross-module overlay proposal is about to formally recognize @_exported as the counterpart for re-exporting modules that are imported.

Since @usableFromInline internal and public differ precisely in that the former declaration isn't available as an API, it seems to me that it'd be unfortunate to use the import/export terminology that means exactly the opposite.

Working with SceneKit recently I've found myself writing code like this:

    let qq = GLKQuaternionMakeWithAngleAndAxis(…)
    let q = SCNQuaternion(qq.x, qq.y, qq.z, qq.w)
    ea.localRotate(by: q)

this led me to write:

extension SCNQuaternion {
    init(_ inQ: GLKQuaternion) {
        self.init(inQ.x, inQ.y, inQ.z, inQ.w)
    }
}

Which immediately gave me the urge to add @inlineable to it, but not really knowing how it works, I decided to google, and found this thread.

Arguably the problem here is that SCNQuaternion lacks a bunch of convenience methods, but should I bother adding @inlineable or @inline(__always) (or both)? Ostensibly the compiler could inline both GLKQuaternionMakeWithAngleAndAxis and the SCNQuaternion construction and then optimize a lot of extra stuff away. Is it likely to do so, even without the annotations?

The general guidance here is to try without it, trace performance, and if there's a specific issue that inlining can resolve, use inlining as needed. If your extension is defined in the same module that you're using it, it probably doesn't matter. It may matter in other circumstances, depending on how it's used and how your project is set up.

This particular init is basically a no-op in disguise, so inlining it won't have most of the downsides that inlining can have, so the risk of being overzealous is pretty small in this case. That's not always true.

7 Likes