Is it permitted to remove `@inlinable` from a function?

In the standard library, if an existing public function has the @inlinable attribute, is it okay to remove that attribute from that function in a future release?

1 Like

Yes and no. It depends on the function.

If something is inlinable, you can remove its inlinability but you must never remove any implicit invariants that it relied on while inlinable, because inlinable code in the wild may exist forever and needs to keep working. So for example, if something was inlinable and accessed the stored properties of a @frozen struct, that struct must remain @frozen and you must not add new members to it even though it is no longer being accessed from inlinable code.

As a rule of thumb, it's safe(r) to remove @inlinable from protocol extensions, but more dangerous to remove it from concrete methods. In either case, it's important to consider what the inlinable code has always done, and leave a comment about it where you've removed the annotation.

3 Likes

Here is an example of a safe @inlinable removal:

extension Substring: LosslessStringConvertible {
   @inlinable
   public init(_ content: String) {
     self = content[...]
   }
}

The initializer here merely assigned self = content[...], which relies on no internals, only stable public functionality. There is no risk from removing the inlining because nothing the code used to do could possibly be invalidated in the future.

4 Likes

Thanks Ben.

I’m working on the floating-point random(in:using:) methods, which are in a constrained extension of BinaryInteger where RawSignificand: FixedWidthInteger.

I’m replacing the bodies of those two functions with calls to a new function that I’ve written, but that new function is an implementation detail and probably shouldn’t be @usableFromInline, so I want to remove @inlinable from the floating-point random(in:using:) methods.

Ah. I think there's some misunderstanding here:

All @usableFromInline functions are, by definition, implementation details. The purpose of that annotation is to allow internal implementation details to be called from inlinable functions.

For generic implementations of numeric functions like random(in:using) the inlinability is absolutely performance critical and cannot be removed. It is what allows callers to specialize the code into a function that operates on the concrete type. Further, any refactoring of that type needs to preserve that specialization capability. So if your helper function is concrete (such as wrapper of a call to the system's random number generator) it might be ok for it to not itself be inlinable. But if that helper function is generic, it will likely also need to be inlinable, and thus specializable.

1 Like

Oh, I should add, @inlinable implies @useableFromInline so you can't remove it from internal functions without replacing it with that. But the ABI checker ought to prevent you from landing a PR that does this.

I was under the impression that @usableFromInline makes the function part of the ABI.

Yes, the helper functions are generic over the random number generator.

Yes, that's what it is for. It is an annotation that allows inlinable functions to be emitted into the caller, but then call functions that otherwise wouldn't have public symbols because they are internal (i.e. implementation details).

1 Like

It may be worth mentioning the next problem you will hit, which is that you will need to mark these new inlinable helper functions with availability. Which will mean you can't call them from inlinable functions that have less restrictive availability, such as the existing random functions. You'll need to use @_alwaysEmitIntoClient for that.

1 Like

Hmm, that is indeed something to think about.

I was hoping that the helper methods could be purely internal to the standard library, but it sounds like the exact opposite is the case, and they’d have to become part of the ABI, and be inlinable, and always get emitted into clients.

…or, wait, do I have it backwards?

If a function is always emitted into the client, does that mean no external code will ever call it through the standard library’s binary interface, and thus the function does not actually need to be part of the ABI?

Because if that’s how it works, then @_alwaysEmitIntoClient would do exactly what I want for the helper methods.

Essentially, I am confident that my changes are an improvement to the existing implementation, but I am also cognizant that it is possible someone might devise an even better approach at some future time.

If that occurs, then clients which have already inlined my implementation will be fine, they can keep using it, just like clients which have already inlined the existing implementation will be fine continuing to use that.

(It’s worth noting that the current implementation crashes on some valid inputs, fails to produce some valid outputs, and does not follow the proposed and documented semantics. My changes will solve all of these issues.)

Edit:

I just read through the Library Evolution document in the Swift repo, and the section on @_alwaysEmitIntoClient indicates that indeed, a declaration marked with that attribute “is not part of the module's ABI”. So it does exactly what I want.

Thanks for letting me know the right way to go about this, Ben.