@inlinable Properties are one of Swift's lesser-known properties. Like others of its kind, its purpose is to enable a specific set of micro-optimizations that you can use to improve the performance of your application. Let's see how this works.

Inline expansion with @inline in Swift

Perhaps the most important thing to note is that while @inlinable is related to code inlining, it is not the same as the @inline property we've covered before. But to save you from having to read two articles, we'll go over the concepts again before going into @inlinable .

In programming, inline expansion, also known as inlining, is a compiler optimization technique that replaces method calls with the body of said method.

The operation of calling a method is difficult to do without performance overhead. As we described in our article on memory allocation, when an application wants to push a new stack trace to a thread, there is a lot of orchestration to transfer, store, and change the state of the application. While stack traces can, in one sense, simplify the debugging process, you might wonder if it's necessary to do this every time. If a method is too simple, the overhead of calling it may not only be unnecessary, but detrimental to the overall performance of the application:

 func printPlusOne(_ num: Int) {
    print("My number: \(num + 1)")
}

print("I'm going to print some numbers!")
printPlusOne(5)
printPlusOne(6)
printPlusOne(7)
print("Done!")

Methods like printPlusOne are too simplistic to justify a full definition in the application's binary. Just for clarity, we define it in the code, but it's best to get rid of it when publishing this application, and replace all places where it is called with the full implementation, like this:

 print("I'm going to print some number!")
print("My number: \(5 + 1)")
print("My number: \(6 + 1)")
print("My number: \(7 + 1)")
print("Done!")

This reduction in method invocation overhead may improve the performance of the application, but may increase the overall package size depending on the size of the method being inlined (which can be understood as replacement, more code in the replaced method, natural code more quantity). This process is done automatically by the Swift compiler, to a variable degree depending on the optimization level your project was built with. @inline attribute can be used to ignore optimization levels and force the compiler to follow a specific direction. Inlining can also be used for obfuscation.

What is the purpose of @inlinable ?

Most optimizations, like inlining, are mostly done internally. While it's guaranteed that the module we're developing will be properly optimized, when we're dealing with calls from other modules, things get a lot more complicated.

Compiler optimization happens because the compiler has complete knowledge of what is being compiled, but when building the framework, it is impossible for the compiler to know how the importer will use it. So while the internal code of the framework will be optimized, the public interface will most likely remain unchanged.

We might think that we can tell the compiler the source information that composes the framework, so that it can retrieve the framework's information and optimize it. But this gets complicated when we realize that the framework's importer is linking against something that has already been compiled. All the information on the source files is gone, and if this is a 3rd party framework, it might not even be there in the first place. (Can't get all the source information of the framework)

This is not an impossible problem. Although the compiler has many ways to solve this problem, the essence is the same, that is to make the public interface of the module contain more information available to the compiler when linking, so that further optimization can be achieved. The code in the framework is not limited to inlining.

In practice, you may notice that doing this can get very out of hand. If we add information to each method of the public interface, it will make the framework larger and most of it will be wasted. We don't know how the framework will be used, so we can't generalize about this.

Swift will let you decide to handle this kind of problem. @inlinable property allows you to enable cross-module inlining for a specific method. After doing so, the method's implementation will be exposed as part of the module's public interface, allowing the compiler to further optimize calls from different modules.

 @inlinable func printPlusOne(_ num: Int) {
    print("My number: \(num + 1)")
}

If an inline method happens to call other methods internally, the compiler will ask you to expose those methods as well. This can be done by marking them as @inlinable or @usableFromInline . @usableFromInline indicates that although the method is used in an inline method, it itself should not really be inlined.

 @inlinable func myMethod() {
    myMethodHelper()
}

// Used within an inlinable method, but not inlinable itself
@usableFromInline internal func myMethodHelper() {
    // ...
}

The biggest benefit of using @inlinable is that some methods may have performance overhead, although most methods have a negligible overhead, but the ones that include generics and must report are very expensive.

 @inlinable public func allEqual<T>(_ seq: T) -> Bool where T : Sequence, T.Element : Equatable {
 // ...
}

Compilers have applied a variety of methods to solve the problem of generics. But as we said, these methods don't apply when called within a separate module. In this case, using @inlinable can bring performance improvements, but the package size will increase.

On the other hand, when you build, @inlinable can have big problems. When the implementation of a method marked @inlinable changes, the module that imports it cannot use its changes unless it is recompiled. Usually you can update the framework by simply replacing the binary, but since the implementation of some methods is inlined, the application will continue to run the old version even if you link against the new version. Therefore, applications that enable the library evolution setting may find themselves unable to use @inlinable , as this may break the framework's ABI stability.

Should @inlinable or @inline be used?

Unless you're building a framework with ABI/API stability, these properties should be completely safe. However, I strongly advise you not to use them unless you know what you are doing. They are built for use in very specific situations that most applications cannot experience.


Sunxb
83 声望330 粉丝