Swift 标准库中的 Attributes

2019-06-19

在看标准库源码的过程中,出现过很多在平时开发时都没有接触过属性(Attribute),如 @inline, @usableFromInline, @inlinable, @frozen 等等,今天就来看看这些属性所代表的含义以及其作用是什么。

@inline

在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。

以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
func calculateAndPrintSomething() {
var num = 1
num *= 10
num /= 5
print("My number: \(num)")
}

print("I'm going to do print some number")
calculateAndPrintSomething()
print("Done!")

假设 calculateAndPrintSomething 方法并没有在其它地方被调用,它在这里的作用仅仅是为了让代码看起来更清晰,因此它没有必要出现在编译后的二进制文件中。

通过函数内联,编译器会将函数调用处直接替换为函数体:

1
2
3
4
5
6
7
//The compiled binary version of the above example
print("I'm going to do print some number")
var num = 1
num *= 10
num /= 5
print("My number: \(num)")
print("Done!")

根据设置的优化等级,Swift 编译器会自动处理: 当为了更快的编译速度时会倾向使用内联,当为了更优化的编译后二进制文件大小时,编译器会更倾向不适用内联,因为内联会在不同的调用地方造成代码的重复,增加编译后二进制文件的大小。

尽管编译器可以自己决定是否进行内联,但 @inline 可以强制编译器改变内联的行为。它的使用方法有两种:

  • @inline(__always) : 告诉Swift 编译器如果可能,总是内联此方法
  • @inline(never) : 告诉编译时永远不要内联这个方法

而根据 Apple 工程师的说法,应该避免使用这个属性。尽管这个属性并不限制使用而且在 Swift 的源码中被广泛使用,但它的公共使用还没有官方正式支持。

尽管编译器会根据优化的设置来决定是否内联,但在某些情况下,你可能需要一种方法来手动决策。这时 @inline 就可以帮助到你。

例如,在优化速度时,似乎编译器会对一些内容并不是很短的方法进行内联,从而导致二进制大小增加。在这种情况下,@inline(never) 可用于防止这个,同时保证二进制文件的速度。

另一个更实际的例子是,你可能想防止黑客接触到一个包含某种敏感信息的方法,它是否会使代码变慢或包变大都无关紧要。你肯定会尝试混淆你的代码来使代码更难理解,或者可以选择混淆工具,例如 SwiftShield,但 @inline(__always) 可以轻松实现这一点而同时不会损害你的代码,我将在下面详细介绍了这个例子:

假设我们有一个音乐应用且一些操作只开放给高级用户。 isUserSubscribed(_:) 方法用来验证用户是否订阅并返回一个布尔值。

1
2
3
4
5
6
7
8
9
10
11
12
func isUserSubscribed() -> Bool {
// 一些复杂的验证逻辑
return true
}

func play(song: Song) {
if isUserSubscribed() {
// 如果用户订阅,播放歌曲
} else {
// 让用户订阅
}
}

这种方式没有什么问题,但如果我们把这个 App 进行反编译并搜索 play(_:) 方法的程序集会发生什么:

play_disassemble

如果我是一个黑客试图破解这个 App 的订阅,看看 play(_:) 方法我就知道 isUserSubscribed(_:) 返回的布尔值控制着 App 的订阅。

我现在可以通过查找 isUserSubscribed(_:) 并强制它返回 true 就可以解锁 App 的全部高级内容:

unlock_primium

在这种情况下,可能因为该方法在 App 里广泛使用,所以编译器决定不内联它。这种决定就造成了一个安全漏洞,使得 App 能够很容易地被逆向工程破解。

现在看看给 isUserSubscribed(_ :) 添加了 @inline(__always) 后会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
@inline(__always) func isUserSubscribed() -> Bool {
// 一些很复杂的验证逻辑
return true
}

func play(song: Song) {
if isUserSubscribed() {
// 播放歌曲
} else {
// 让用户订阅
}
}

after_inline

同样的 play(_:) 方法里现在不包括对订阅状态的判断。这个方法调用完全被其内部的 “复杂的验证” 所取代,这样反编译后看起来变得更加复杂,订阅也更加难以破解。

好处是,由于每次调用 isUserSubscribed(_:) 都被复杂的验证取代,因此就没有一种方法可以解锁应用程序的整个订阅,黑客现在必须破解每一个进行验证的方法。当然,多处的重复的代码也意味着我们的二进制文件会变得更大。

请注意,使用 @inline(__always) 并不能保证编译器会真正内联你的方法。它的规则是未知的,例如在无法避免动态派发的情况下就无法进行内联。

inlinable

给函数、方法、计算属性、下标脚本、便捷初始化器或者析构器的声明中应用这个特性,将这个声明的实现作为模块的公开接口暴露出去。这就允许编译器可以在其它模块中将具体函数调用替换为为这个函数体,即进行内联。inlinable 可以应用于 publicinternal 类型的声明。

可内联的函数体可以引用任何模块中的 public 声明以及跟相同模块中被标记为 usableFromInline 属性的 internal 声明,但是不可以引用 private 以及 fileprivate 的声明。

这个特性不能应用在内嵌函数或者 fileprivateprivate 声明中。定义在内联函数内的函数和闭包会隐式地允许内联,尽管它们没有被标记这个特性。

需要注意的是出现这个属性并不强制进行内联或者其它的优化,同时当只有一个模块时也不会有任何优化的效果。

usableFromInline

给函数、方法、计算属性、下标脚本、初始化器或者析构器声明添加这个属性,允许其可以不用暴露为模块的接口,而用在同一个模块中声明的可内联的代码里。可以应用在拥有 internal 访问级别的修饰符上。

类似 public 访问级别修饰符,这个特性暴露声明作为模块的公开接口。但与 public 不同的是,尽管声明的符号已经暴露出来, 编译器也不能在模块外的代码中引用 usableFromInline 标记的声明。总之,模块外的代码依旧能通过运行时行为与声明的符号进行沟通。

inlinable 标记的声明会隐式地在行内代码可用。尽管不论是 inlinable 还是 usableFromInline 都能应用于 internal 声明,但你同时使用这两个特性是错误的。

@frozen

struct 类型的 @frozen

当库作者确定以后永远不需要向结构添加字段时,他们可能会将该类型标记为 @frozen

这将允许编译器在编译时优化一些本来需要在运行时进行的调用(例如,它可以在没有间接的情况下直接访问字段)。

在使用二进制稳定性模式进行编译时,只要满足以下所有条件,就可以将结构标记为 @frozen

  • 该结构是ABI-public(参见SE-0193),即 public 或标记为 @usableFromInline
  • 结构字段类型中提到的每个类,枚举,结构,协议或类型都是ABI-public。
  • 没有字段有观察访问者(willSetdidSet)。
  • 如果字段具有初始值,则计算初始值的表达式不引用任何不是ABI-public的类型或函数。

标记一个 struct@frozen 仅保证其存储的实例属性不会更改。这允许编译器执行某些优化,例如忽略从未访问过的属性,或者从同一实例属性中消除冗余负载。但是,它没有提供 C struct 可能的一些其他保证:

  • 它不能保证 “trivial”(在 C++ 意义上)。包含类引用或闭包的冻结结构在复制时以及超出范围时仍需要引用计数。

  • 它不一定具有已知的大小或对齐方式。范型冻结 struct 的布局可能取决于运行时提供的泛型参数。

  • 即使是具体的实例化也可能没有已知的大小或对齐方式。具有非冻结字段的冻结 struct 在运行时之前可能不知道具体大小。

  • 不保证使用与 C struct 使用相同的布局。如果需要这样的 struct,则应该在 C 头中定义它并导入到 Swift 中。

  • 这些字段不保证按声明顺序排列。编译器可以选择对字段重新排序,例如在满足对齐要求的同时最小化填充。

也就是说,允许编译器使用 struct 内容和布局的知识来导出任何这些属性。但是,基于此在语言级别并不支持,但有两个例外:

  1. 具有单个字段的结构的运行时内存布局总是与实例属性的布局相同,无论结构是否声明为 @frozen。从Swift 1开始就是如此。(但是这并没有扩展到调用约定。如果结构没有被冻结,它将被间接传递,即使它的单个字段被冻结,因此可以直接传递)

  2. 在 C / Objective-C 中 “可空” 的任何类型的 nil 表示与这些语言中 nilNULL 的表示相同。这包括类引用,类绑定协议引用,类类型引用,不安全指针,@convention(c)函数,@convention(block) 函数,OpaquePointerSelectorNSZone。自Swift 3(SE-0055)以来,情况确实如此。

enum 类型的 @frozen

将枚举标记为 @frozen 将同样允许编译器优化运行时调用。

此外,将枚举标记为 @frozen 会恢复库的使用者在没有 @unknown 默认值的情况下彻底切换该枚举的能力,因为它保证不会添加其他 case。

一旦被冻结,对枚举 case 所做的所有更改都会影响其ABI。

@discardableResult

默认情况下,如果调用一个函数,但函数的返回值并未使用,那么编译器会发出警告。你可以通过给函数使用 @discardableResult 来忽略警告。

@_transparent

@_transnsparent@inlinable 类似,告诉编译器在需要的时候可以将声明的函数内联。

这样做有几个后果:

  • 任何对标记为 @_transparent 的函数的调用都必须内联,即使在 -Onone 下也是如此。
  • 因此,@_transparent 函数是隐式可内联的,因为更改其实现很可能不会影响现有编译二进制文件中的调用者。
  • 一个 public 的或 @usableFromInline @_transparent 函数必须只引用公共符号,并且不得根据其所在模块的认知进行优化。[前者由Sema中的检查捕获。]
  • 调试信息当单步调试执行调用函数时,将跳过内联的操作。

这就是 @_transparent 的意思。

你什么时候应该使用 @_transparent

  • 这个函数实现的功能是否需要改变?如果是的话那么你不可以允许它进行内联。
  • 实现是否需要调用私有属性,包括 private 函数以及可能在下一个版本中消失的 internal 函数? 那么你不能允许它内联。
  • 如果函数不进行内联的话可以吗?如果你更希望这个函数是内联的话,那么你应该使用 @inlinable,而不是 @_transparent。(如果你真的需要这个,你也可以添加 @inline(__ always)。)
  • 如果函数在 -Onone 模式下都可以内联的话是个问题吗? 那么与前一种情况类似。相信编译器。
  • 如果您无法单步执行这个被内联的函数,是否会有问题? 如果是,那么你想要的不是 @_transparent, 你只是需要 @inline(__always)(也可能是 @inlinable,用于跨模块内联)。
  • 如果在所有数据流诊断之后发生内联,是否可以?如果可以,那么同上,只需要 @inline(__always)即可。

如果你能做到这些,那么听起来 @_transparent 是正确的选择。

与其他注解的交互

  • 如上所述,将 @_transparent 放在 public@usableFromInline 的函数上会将其主体暴露给其他模块。没有必要另外包括 @inlinable
  • 但是,与 @inlinable 不同,@_transnsparent 并不意味着 @usableFromInline。可以将标记为 @_transparent 的函数仅用于当前模块,甚至是当前文件。

目前实现的限制

  • 在非单前端模式下编译时,不会为主文件(对于每个前端调用)中的任何函数生成 SIL,包括 @inline(__always)@_transparent 函数,这意味着它们不会被内联。这在语义上是一个错误。 rdar://problem/15366167

_semantics()

我们使用 @_semantics 属性来标注标准库中的代码。高级 SIL 优化器可以使用这些注释来执行特定域的优化。相同的函数可能具有多个 @_semantics 属性。

这是 @_semantics 属性的一个示例:

1
2
3
4
@public @_semantics("array.count")
func getCount() -> Int {
return _buffer.count
}

在此示例中,我们使用标记 array.count 注释 Swift 数组结构的成员。此标记通知优化器此方法读取数组的大小。

@_semantics 属性允许我们定义在 Swift 代码中实现 “内置” SIL 级操作。在 SIL 代码中,它们被编码为应用指令,但优化器可以作为原子指令对它们进行操作。语义注释不一定需要在公共API上。例如,Array下标运算符可以在语义模型中调用两个操作。一个用于检查边界,另一个用于访问元素。通过这种抽象,优化器可以删除 checkSubscript 指令并保留 getElement 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
@public subscript(index: Int) -> Element {
get {
checkSubscript(index)
return getElement(index)
}

@_semantics("array.check_subscript") func checkSubscript(_ index: Int) {
...
}

@_semantics("array.get_element") func getElement(_ index: Int) -> Element {
return _buffer[index]
}

Swift优化器可以访问 @_semantics 属性提供的信息以执行高级优化。在优化流水线的早期阶段,优化器不会使用特殊语义内联函数,以便允许早期的高级优化传递对它们进行操作。在优化流水线的后期阶段,优化器使用特殊语义内联函数以允许低级优化。

References

  1. The Forbidden @inline Attribute in Swift
  2. [swift-users] inline best practices?
  3. TransparentAttr
  4. Attributes
  5. Cross-module inlining and specialization
  6. Library Evolution for Stable ABIs