Hi all,

Here is a draft proposal that makes public a feature we’ve had for a while. Let 
me know what you think!

Cross-module inlining and specialization ("@inlinable")
Proposal: SE-NNNN <file:///Users/slava/NNNN-filename.md>
Authors: Slava Pestov <https://github.com/slavapestov>, Jordan Rose 
<https://github.com/jrose-apple>
Review Manager: TBD
Status: Initial pitch
Implementation: Already implemented as an underscored attribute @_inlineable
Introduction
We propose introducing an @inlinable attribute which exports the body of a 
function as part of a module's interface, making it available to the optimizer 
when referenced from other modules.

Motivation
One of the top priorities of the Swift 5 release is a design and implementation 
of the Swift ABI. This effort consists of three major tasks:

Finalizing the low-level function calling convention, layout of data types, and 
various runtime data structures. The goal here is to maintain compatibility 
across compiler versions, ensuring that we can continue to make improvements to 
the Swift compiler without breaking binaries built with an older version of the 
compiler.

Implementing support for library evolution, or the ability to make certain 
source-compatible changes, without breaking binary compatibility. Examples of 
source-compatible changes we are considering include adding new stored 
properties to structs and classes, removing private stored properties from 
structs and classes, adding new public methods to a class, or adding new 
protocol requirements that have a default implementation. The goal here is to 
maintain compatibility across framework versions, ensuring that framework 
authors can evolve their API without breaking binaries built against an older 
version of the framework. For more information about the resilience model, see 
the library evolution document 
<https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst> in the 
Swift repository.

Stabilizing the API of the standard library. The goal here is to ensure that 
the standard library can be deployed separately from client binaries and 
frameworks, without forcing recompilation of existing code.

All existing language features of Swift were designed with these goals in mind. 
In particular, the implementation of generic types and functions relies on 
runtime reified types to allow separate compilation and type checking of 
generic code.

Within the scope of a single module, the Swift compiler performs very 
aggressive optimization, including full and partial specialization of generic 
functions, inlining, and various forms of interprocedural analysis.

On the other hand, across module boundaries, runtime generics introduce 
unavoidable overhead, as reified type metadata must be passed between 
functions, and various indirect access patterns must be used to manipulate 
values of generic type. We believe that for most applications, this overhead is 
negligible compared to the actual work performed by the code itself.

However, for some advanced use cases, and in particular for the standard 
library, the overhead of runtime generics can dominate any useful work 
performed by the library. Examples include the various algorithms defined in 
protocol extensions of Sequence and Collection, for instance the mapmethod of 
the Sequence protocol. Here the algorithm is very simple and spends most of its 
time manipulating generic values and calling to a user-supplied closure; 
specialization and inlining can completely eliminate the algorithm of the 
higher-order function call and generate equivalent code to a hand-written loop 
manipulating concrete types.

We would like to annotate such functions with the @inlinable attribute. This 
will make their bodies available to the optimizer when building client code; on 
the other hand, calling such a function will cause it to be emitted into the 
client binary, meaning that if a library were to change the definition of such 
a function, only binaries built against the newer version of library will use 
the new definition.

Proposed solution
The @inlinable attribute causes the body of a function to be emitted as part of 
the module interface. For example, a framework can define a rather impractical 
implementation of an algorithm which returns true if all elements of a sequence 
are equal or if the sequence is empty, and falseotherwise:

@inlinable public func allEqual<T>(_ seq: T) -> Bool
    where T : Sequence, T.Element : Equatable {
  var iter = seq.makeIterator()
  guard let first = iter.next() else { return true }

  func rec(_ iter: inout T.Iterator) -> Bool {
    guard let next = iter.next() else { return true }
    return next == first && rec(&iter)
  }

  return rec(&iter)
}
A client binary built against this framework can call allEqual() and enjoy a 
possible performance improvement when built with optimizations enabled, due to 
the elimination of abstraction overhead.

On the other hand, once the framework author comes to their senses and 
implements an iterative solution to replace the recursive algorithm defined 
above, the client binary cannot make use of the more efficient implementation 
until recompiled.

Detailed design
The new @inlinable attribute can only be applied to the following kinds of 
declarations:

Functions and methods
Subscripts
Computed properties
Initializers
Deinitializers
The attribute can only be applied to public declarations. This is because the 
attribute only has an effect when the declaration is used from outside of the 
module. Within a module, the optimizer can always rely on the function body 
being available.

For similar reasons, the attribute cannot be applied to local declarations, 
that is, declarations nested inside functions or statements. However, local 
functions and closure expressions defined inside public @inlinable functions 
are always implicitly @inlinable.

When applied to subscripts or computed properties, the attribute applies to the 
getter, setter, didSetand willSet, if present.

The compiler will enforce certain restrictions on bodies of inlinable 
declarations:

inlinable declarations cannot define local types. This is because all types 
have a unique identity in the Swift runtime, visible to the language in the 
form of the == operator on metatype values. It is not clear what it would mean 
if two different libraries inline the same local type from a third library, 
with all three libraries linked together into the same binary. This becomes 
even worse if two different versions of the same inlinable function appear 
inside the same binary.

inlinable declarations can only reference other public declarations. This is 
because they can be emitted into the client binary, and are therefore limited 
to referencing symbols that the client binary can reference.

Note: The restrictions enforced on the bodies of @inlinable declarations are 
exactly those that we have in place on default argument expressions of public 
functions in Swift 4.

Source compatibility
The introduction of the @inlinable attribute is an additive change to the 
language and has no impact on source compatibility.

Effect on ABI stability
The introduction of the @inlinable attribute does not change the ABI of 
existing declarations. However, adding @inlinable to an existing declaration 
changes ABI, because the declaration will no longer have a public entry point 
in the generated library. Removing @inlinable from an existing declaration does 
not change ABI, because it merely introduces a new public symbol in the 
generated library.

We have discussed adding a "versioned @inlinable" variant that preserves the 
public entry point for older clients, while making the declaration inlinable 
for newer clients. This will likely be a separate proposal and discussion.

Effect on API resilience
Because a declaration marked @inlinable is not part of the library ABI, 
removing such a declaration is a binary-compatible, but source-incompatible 
change.

Any changes to the body of a declaration marked @inlinable should be considered 
very carefully. As a general guideline, we feel that @inlinable makes the most 
sense with "obviously correct" algorithms which manipulate other data types 
abstractly through protocols, so that any future changes to an @inlinable 
declaration are optimizations that do not change observed behavior.

Comparison with other languages
The closest language feature to the @inlinable attribute is found in C and C++. 
In C and C++, the concept of a header file is similar to Swift's binary 
swiftmodule files, except they are written by hand and not generated by the 
compiler. Swift's public declarations are roughly analogous to declarations 
whose prototypes appear in a header file.

Header files mostly contain declarations without bodies, but can also declare 
static inlinefunctions with bodies. Such functions are not part of the binary 
interface of the library, and are instead emitted into client code when 
referenced. As with @inlinable declarations, static inlinefunctions can only 
reference other "public" declarations, that is, those that are defined in other 
header files.

Alternatives considered
One possible alterative would be to add a new compiler mode where all 
declarations become implicitly @inlinable.

However, such a compilation mode would not solve the problem of delivering a 
stable ABI and standard library which can be deployed separately from user 
code. We don't want all declaration bodies in the standard library to be 
available to the optimizer when building user code.

While such a feature might be useful for users who build private frameworks 
that are always shipped together their application without resilience concerns, 
we do not feel it aligns with our goals for ABI stability, and at best it 
should be a separate discussion.

For similar reasons, we do not feel that an "opt-out" attribute that can be 
applied to declarations to mark them non-inlinable makes sense.

We have also considered generalizing @inlinable to allow it to be applied to 
entire blocks of declarations, for example at the level of an extension. As we 
gain more experience with using this attribute in the standard library we might 
decide this would be a useful addition, but we feel that for now, it makes 
sense to focus on the case of a single inlinable declaration instead. Any 
future generalizations can be introduced as additive language features.

We originally used the spelling @inlineable for the attribute. However, we 
settled on @inlinable for consistency with the Decodable and Encodable 
protocols, which are named as they are and not Decodeable and Encodeable.

Finally, we have considered some alternate spellings for this attribute. The 
name @inlinable is somewhat of a misnomer, because nothing about it actually 
forces the compiler to inline the declaration; it might simply generate a 
concrete specialization of it, or look at the body as part of an 
interprocedural analysis, or completely ignore the body. We have considered 
@alwaysEmitIntoClient as a more accurate, but awkward, spelling of the 
attribute's behavior.
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to