Date | Notes |
---|---|
2020-07-26 | 首次提交 |
Preface
《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的内联函数。
编译器优化等级
由于内联函数会将函数调用展开为函数体,因此当编译器内联某一函数时,该函数本身将不再会被调用。而在 Debug 模式下,由于我们经常会使用打断点等调试手段,如果此时内联将不利于我们排查问题。因此在 Debug 模式下,编译器默认将不进行内联。
控制编译器优化等级的设置位于:Xcode - TARGETS - Build Settings - Swift Compiler - Optimization Level,其中便会影响 Swift 函数是否内联:
Swift 编译器所支持的优化等级具体如下:
Level | Part |
---|---|
[-Onone] | 无优化(Debug 模式默认) |
[-O] | 速度优先(Release 模式默认) |
[-Osize] | 体积优先 |
经过实际测试,如下 foo
函数在 [-O]
与 [-Osize]
等级下均被优化:
func foo() {
print("foo") // BREAKPOINT 2 🔴
foo() // BREAKPOINT 1 🔴
在 [-Onone]
等级的汇编如下(Xcode Menu - Debug - Debug Workflow - Always Show Disassembly):
; [-Onone]
demo`main:
0x100000e50 <+0>: pushq %rbp
0x100000e51 <+1>: movq %rsp, %rbp
0x100000e54 <+4>: subq $0x10, %rsp
0x100000e58 <+8>: movl %edi, -0x4(%rbp)
0x100000e5b <+11>: movq %rsi, -0x10(%rbp)
; 断点 1 ⬇:函数调用(内联后则不存在该行汇编指令)
-> 0x100000e5f <+15>: callq 0x100000e70 ; demo.foo() -> () at main.swift:11
0x100000e64 <+20>: xorl %eax, %eax
0x100000e66 <+22>: addq $0x10, %rsp
0x100000e6a <+26>: popq %rbp
0x100000e6b <+27>: retq
demo`foo():
0x100000e70 <+0>: pushq %rbp
0x100000e71 <+1>: movq %rsp, %rbp
0x100000e74 <+4>: subq $0x30, %rsp
; 断点 2 ⬇:作用域在 foo() 内
-> 0x100000e78 <+8>: movq 0x189(%rip), %rax ; (void *)0x00007fff8b1517e0: type metadata for Any
; ...
0x100000f17 <+167>: retq
在 [-O]
等级的汇编如下:
; [-O]
demo`main:
; ...
; 断点 2 ⬇:作用域在 main 内
-> 0x100000e32 <+50>: movq 0x1c7(%rip), %rax ; (void *)0x00007fff8b149910: type metadata for Swift.String
; ...
0x100000e82 <+130>: retq
在 [-Osize]
等级的汇编如下:
; [-Osize]
demo`main:
; ...
; 断点 2 ⬇:作用域在 main 内
-> 0x100000e3e <+46>: movq 0x1bb(%rip), %rax ; (void *)0x00007fff8b149910: type metadata for Swift.String
; ...
0x100000e8a <+122>: retq
[-O]
与 [-Osize]
的结果类似,即通过内联使得断点 1 不再执行;而断点 2 虽然得到执行,但通过查看汇编可以看出,断点 2 处的代码作用域已变为 main
,且无 call
调用相关函数的指令,证明函数已经内联。
内联条件
在开启编译器优化后,Swift 编译器便会针对函数进行内联优化,但并非所有函数都支持内联。
- 函数体过长的函数将不支持内联;
// [-O]
func foo() {
print(#function)
print(#function)
print(#function)
}
foo() // BREAKPOINT 🔴
由于过长的函数体内联反而会导致实际代码行数增多,可能会影响性能,因此如上,当函数体内的 print
语句超过两句时,内联就会失效。我们可以从函数调用处的断点或者汇编来得出结论:
demo`main:
0x100000da0 <+0>: pushq %rbp
0x100000da1 <+1>: movq %rsp, %rbp
-> 0x100000da4 <+4>: callq 0x100000db0 ; demo.foo() -> () at main.swift:11
0x100000da9 <+9>: xorl %eax, %eax
0x100000dab <+11>: popq %rbp
0x100000dac <+12>: retq
- 包含递归调用的函数将不支持内联;
// [-O]
func foo(_ i: Int) -> Int {
if i <= 1 {
return i
}
var result = foo(i - 1)
result += 1
return result
}
_ = foo(5)
由于包含递归的函数体需要在函数内部调用自身,此时若使用内联便会造成无限展开。因此如上,当函数体内的存在自身的递归时,内联就会失效。我们可以从函数调用处的断点或者汇编来得出结论:
demo`main:
0x100000f70 <+0>: pushq %rbp
0x100000f71 <+1>: movq %rsp, %rbp
-> 0x100000f74 <+4>: movl $0x5, %edi
0x100000f79 <+9>: callq 0x100000f90 ; demo.foo(Swift.Int) -> Swift.Int at main.swift:41
0x100000f7e <+14>: xorl %eax, %eax
0x100000f80 <+16>: popq %rbp
0x100000f81 <+17>: retq
尾递归
对于递归函数需要注意的是,编译器会对尾递归(Tail Recursion,即递归位于函数末尾)进行额外优化,即尾递归优化(Tail Call Optimization)。如下一段求阶乘的代码:
// [-O] func foo(_ i: Int) -> Int { if i <= 1 { return i } return foo(i - 1) } _ = foo(5)
将其转换为汇编如下:
demo`main: 0x100000f90 <+0>: pushq %rbp 0x100000f91 <+1>: movq %rsp, %rbp -> 0x100000f94 <+4>: xorl %eax, %eax 0x100000f96 <+6>: popq %rbp 0x100000f97 <+7>: retq
本文暂时不会涉及过多关于尾递归的内容。
- 包含动态派发的代码将不支持内联;
class People {
var name: String = "Name"
func bar() {}
func foo() {
// NOT Inline
var p: People = Student()
p = Teacher()
p.bar()
// Inline
let q = Student()
q.baz() // b -r Student.baz 🔴
}
}
class Teacher: People {
override func bar() {
print("Teacher - \(name)")
}
}
class Student: People {
override func bar() {
print("Student - \(name)")
}
func baz() {}
}
People().foo()
由于包含动态派发的函数只有在运行时才是确定的。因此如上,当使用多态时,内联就会失效。我们可以从函数调用处的断点或者汇编来得出结论:
demo`main:
; ...
0x100001524 <+212>: movq 0x18(%rbx), %rdi
0x100001528 <+216>: callq 0x100001be2 ; symbol stub for: swift_bridgeObjectRelease
-> 0x10000152d <+221>: callq 0x100001960 ; demo.Teacher.bar() -> ()
0x100001532 <+226>: movq %r13, %rdi
0x100001535 <+229>: callq 0x100001c0c ; symbol stub for: swift_release
; ...
@inline
我们可以使用 @inline
来建议编译器去禁止(never
)或在条件允许的情况下(除递归、动态派发等)总是(__always
)内联,其优先级高于编译器优化。
@inline(never)
@inline(never)
将使得原本在开启编译器优化时会被内联的函数不再内联:
// [-O] / [-Osize]
@inline(never) func bar() {
print("bar")
}
bar() // BREAKPOINT 🔴
此时函数调用处的断点将有效,且转换为汇编也将显示存在 call
指令:
; [-O] / [-Osize]
-> 0x100000e74 <+4>: callq 0x100000e80 ; demo.bar() -> () at main.swift:1
@inline(__always)
需要注意的是,__always
只在开启编译器优化时几乎总是内联,未开启时将不会被内联。
@inline(__always) func baz() {
print("baz")
}
baz() // BREAKPOINT 🔴
如上代码即使在 [-Onone]
等级下我们也可以通过断点和汇编看出其本质并没有被内联:
; [-Onone]
-> 0x100000e5f <+15>: callq 0x100000e70 ; demo.baz() -> () at main.swift:3
而在 [-O]
或 [-Osize]
下使用 @inline(__always)
则可以将原本因行数过多的函数进行内联:
// [-O] / [-Osize]
@inline(__always) func baz() {
print(#function)
print(#function)
print(#function)
print(#function)
print(#function)
}
baz() // BREAKPOINT 🔴
@inline(__always)
虽然能够「控制」一部分函数进行内联,但其本质是对编译器行为的建议而非强制,因为有些函数并不能够被内联,比如上面递归中的案例。这里我们只需要了解其用处,而在实际开发中很少会用到。