12

Swift 拾遗 - 内联函数

 3 years ago
source link: https://kingcos.me/posts/2020/swift_inline/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Date Notes 2020-07-26 首次提交

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的内联函数。

编译器优化等级

由于内联函数会将函数调用展开为函数体,因此当编译器内联某一函数时,该函数本身将不再会被调用。而在 Debug 模式下,由于我们经常会使用打断点等调试手段,如果此时内联将不利于我们排查问题。因此在 Debug 模式下,编译器默认将不进行内联。

控制编译器优化等级的设置位于:Xcode - TARGETS - Build Settings - Swift Compiler - Optimization Level,其中便会影响 Swift 函数是否内联:

1

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) 虽然能够「控制」一部分函数进行内联,但其本质是对编译器行为的建议而非强制,因为有些函数并不能够被内联,比如上面递归中的案例。这里我们只需要了解其用处,而在实际开发中很少会用到。


Recommend

  • 4
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - Swift Tips

    Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。那么作为起始篇,随着整个系列的进行,其中「遗」漏的基本使用将在本文中得到补充。 Content...

  • 8
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - 方法

    Date Notes 2020-09-20 首次提交 Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的方法。 Swift 中的结构体是值...

  • 9
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - 属性

    Date Notes Info 2020-09-18 纳入 Swift 拾遗系列,并重新整理完善 Swift 5.3, Xcode 12.0 2017-04-27 扩充「延迟存储属性」部分并新增「devxoul/Then」一节 Swift 3.1, Xcode 8.3.2 2016-10-26 首次提交 Swift 3.0, Xcode 8.1 Beta 3 Preface 《Swift...

  • 12
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - 闭包

    Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的闭包(Closure)。 我们将参数和返回值类型为 Int

  • 7
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - 汇编

    Date Notes 2020-08-10 首次提交 Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。本文涉及了各个部分关于汇编的内容。 struct & class

  • 5
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - struct & class

    Date Notes 2020-08-06 首次提交 Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的枚举 struct 和 cl...

  • 12
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - enum

    Date Notes 2020-08-01 首次提交 Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的枚举 enum。 内...

  • 15
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - inout

    Date Notes 2020-07-25 首次提交 Preface 《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究修饰 Swift 中函数参数的 inout 关键...

  • 10
    • kingcos.me 3 years ago
    • Cache

    Swift 拾遗 - 访问控制

    Swift 拾遗 - 访问控制 2021.05.24...

  • 5

    2021年09月27日 阅读 887 写更好的 Swift 代码:技巧拾遗 为了避免命名冲突,在 OC 时代,我们的做法是在方法前面添加

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK