@inlinable
属性是 Swift 鲜为人知的属性之一。与其他同类一样,它的目的是启用一组特定的微优化,您可以使用它们来提高应用程序的性能。让我们来看看这个是如何工作的。
在 Swift 中使用 @inline 进行内联扩展
也许最需要注意的是,虽然 @inlinable
与代码内联有关,但它与我们之前已经介绍过的 @inline
属性不同。但是为了避免您不得不阅读两篇文章,我们将在介绍 @inlinable
之前再次介绍这些概念。
在编程中,内联扩展,也称为内联,是一种编译器优化技术,它用所述方法的主体替换方法调用。
调用方法的操作很难做到没有性能开销。正如我们在关于内存分配的文章中所述,当应用程序希望将新的堆栈跟踪推送到线程时,会进行大量的编排来传输、存储和改变应用程序的状态。虽然从方面来说,堆栈跟踪可以简化调试过程,但您可能想知道是否有必要每次都这样做。如果一个方法太简单,调用它的开销可能不仅是不必要的,而且对应用程序的整体性能有害:
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!")
printPlusOne 这样的方法过于简单,无法在应用程序的二进制文件中证明完整的定义。只是为了清晰起见,我们在代码中定义了它,但在发布这个应用程序时最好去掉它, 用完整的实现替换所有调用它的地方,如下所示:
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!")
这种减少方法调用开销,可能会提高应用程序的性能,但可能会增加整体包大小,具体取决于被内联的方法的大小(可以理解为替换,被替换的方法中的代码多,自然代码量就多了)。这个过程是由 Swift 编译器自动完成的,根据你工程构建时的优化级别,程度是可变的。@inline
属性可以用来忽略优化级别,强制编译器遵循特定的方向。内联也可以用于混淆。
@inlinable 的目的是什么?
大多数优化,如内联,大多在内部完成。虽然可以保证自己正在开发的模块将得到适当的优化,但当我们处理来自其他模块的调用时,事情会复杂得多。
编译器优化之所以发生,是因为编译器对正在编译的内容有完整的了解,但在构建framework时,编译器不可能知道导入方将如何使用他。因此虽然框架的内部代码将得到优化,但公共接口很可能会不变。
我们可能会想,我们可以告诉编译器组成framework的源信息,以便它能够重新获得framework的信息,并对其优化。但当我们意识到该框架的导入器正在链接的是已经编译的东西时,这会变得复杂。源文件上的所有信息都不见了,如果这是第三方框架,甚至可能一开始就没有这些源信息。(拿不到framework所有的源信息)
这不是一个不可能出现的问题,虽然编译器有很多种解决这个问题的方法,但本质是一样的,就是让模块的公共接口在链接时包含更多编译器可用的信息,这样就可以进一步优化framework中的代码,不限于内联。
在实践中,你可能会注意到这样做可能会严重失控。如果我们给公共接口的每个方法都添加信息,这样会让框架的体积增大,而且大部分都会被浪费掉。我们不知道framework将会被如何使用,所以不能一概而论的这样操作。
Swift会让你自己决定来处理这种问题。@inlinable
属性允许您为特定方法启用跨模块内联。这样做了之后,方法的实现将作为模块公共接口的一部分来公开,允许编译器进一步优化来自不同模块的调用。
@inlinable func printPlusOne(_ num: Int) {
print("My number: \(num + 1)")
}
如果内联方法恰好在内部调用其他方法,编译器会要求您也公开这些方法。这可以通过将它们标记为 @inlinable
或 @usableFromInline
。@usableFromInline
表明虽然该方法在内联方法中使用的,但它本身并不真正应该被内联。
@inlinable func myMethod() {
myMethodHelper()
}
// Used within an inlinable method, but not inlinable itself
@usableFromInline internal func myMethodHelper() {
// ...
}
使用 @inlinable
最大的收益就是有些方法可能会有性能开销,尽管大多数的方法这种开销可以忽略不计,但像那种包含泛型和必报的,开销很大。
@inlinable public func allEqual<T>(_ seq: T) -> Bool where T : Sequence, T.Element : Equatable {
// ...
}
编译器已经应用了多种方法来解决泛型的问题。但正如我们说的,在单独的模块内调用,这些方法并不适用。这种情况下, 使用@inlinable
可以带来性能提升,不过包的大小会增加。
另一方面,当你构建时,@inlinable
可能有大问题。一个被标记为 @inlinable
的方法实现有改变时,除非重新编译,要不然导入他的模块无法使用他的修改。 通常你可以通过简单地替换二进制文件来更新框架,但由于某些方法的实现被内联,即使您链接到新版本,应用程序仍将继续运行旧的版本。因此,启用 library evolution
设置的应用程序可能会发现自己无法使用 @inlinable
,因为这可能会破坏框架的ABI稳定性。
应该使用 @inlinable 还是 @inline ?
除非您正在构建一个具有ABI/API稳定性的框架,否则这些属性应该是完全安全的。不过,我强烈建议你不要使用它们,除非你知道自己在做什么。它们是为在大多数应用程序都无法体验的非常特殊的情况下使用而构建的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。