Generics, Swift Local Packages & Whole Module Optimization

I'm a heavy user of both Generics and Swift Local Packages.

Here's my question:

  • How can I be sure that my app is using specialized versions of the generic functions that are defined in different modules — i.e., different local packages?

I compile my Release builds with whole module optimization. But my current understanding is that whole module optimization is compile-time-only and module-only. That makes me think that when I compile and link my overall app I may not be getting cross-module specialization of my generic funcs.

If that's correct, should I be making generous use of @inlinable to ensure cross-module optimization?

(I've tried finding a reliable answer to this, but so far no luck. I know that the SPM Usage page talks about whole module optimization, but only at the module level.)

you as a library consumer won’t get any benefit from using @inlinable in your own module. but the library author ought to be using @inlinable on his/her end to enable cross-module inlining.

if you have multiple modules in your project, @inlinable might help. but keep in mind that it’s not necessarily a win if all it does is push generic abstraction "down a level" in the call stack, since the @usableFromInline calls aren’t inlined.

1 Like

Sorry, I wasn’t as clear as I could have been: I use Swift local packages. In other words, I’ve used SPM to modularize my app’s own code.

So what I meant was: should I liberally add @inlinable declarations to the generic funcs inside those packages so that call sites in the app and other modules are potentially specialized?

Sorry; I read your reply in email, and for whatever reason it didn’t include your second paragraph. I think I understand the caveat.

In your instance you may also want to add -cross-module-optimization to your Swift flags in Xcode, which should help matters.

1 Like

Thank you, I didn't know about that.

For those interested in learning more, the pull request for the (still experimental?) setting is here:

A new SIL module pass "annotates" functions and types with @inlinable and @usableFromInline.

A comment here (by the author of the pull request) says that it can also be enabled using the undocumented Xcode build setting SWIFT_CROSS_MODULE_OPTIMIZATION.

I'm not sure about the impact on runtime performance yet, but it definitely has a big impact on buildtime performance. :-)

The answer is that @inlinable is about defining ABI boundaries on libraries vended as binaries. You are not supposed to need the attribute at all for libraries vended as source, because in that context it is a meaningless distinction. However, as long as -cross-module-optimization remains experimental and not the default, @inlinable’s side effect of punching a hole in the module boundary can help let the optimizer through too (which is help the optimizer ought not to need).

What I consider best practice at the moment for a source‐vended library is to ignore it until you end up with a piece of code that demonstrably is not fast enough. At that point, try -cross-module-optimization to see if the module barrier is part of the problem. If it turns out to be the case, then you will be able to get the same improvement by applying @inlinable to the related code, and doing so will be an infinitely better experience for your clients than telling them to use an experimental flag. You can remove them again at a future date when the standard optimizer has been improved.

For most code, applying @inlinable everywhere manually* is too much effort for the little benefit it brings. But the only real danger from doing so is that if you ever decide to vend your library as a binary (or someone forks your work to do so), there will be a lot of cleaning up to do.

*It just occurred to me that it might now be feasible to automate with a plug‐in. If anyone else tries this before I get around to it, let me know how if works out.