【问题标题】:Which dispatch method would be used in Swift?在 Swift 中将使用哪种调度方法?
【发布时间】:2018-01-24 12:34:35
【问题描述】:

在WWDCUnderstanding Swift Performance中,当对象的类型是协议时声明:调用协议所需的函数将使用Existential容器调度方法。

protocol MyProtocol {
    func testFuncA()
}

extension MyProtocol {
    func testFuncA() {
        print("MyProtocol's testFuncA")
    }
}

class MyClass: MyProtocol {}

// This use Existential Container, find implementation through PWT.
let object: MyProtocol = MyClass()
object.testFuncA()

我的问题来了:当 object 被指定为 MyClass 时,Swift 是如何找到实现的?我对这个问题有两种解释。

  1. 是否将扩展的默认实现复制到 MyClass 的 v-table,并通过 MyClass 的 v-table 调度方法?

  2. 是否仍然使用 Existential 容器调度方法,并且 Existential 容器的 PWT 包含扩展的默认实现?

// Use Dynamic Dispatch or Static Dispatch? How?
let object: MyClass = MyClass()
object.testFuncA()

【问题讨论】:

标签: ios swift


【解决方案1】:

在这个例子中:

protocol MyProtocol {
    func testFuncA()
}

extension MyProtocol {
    func testFuncA() {
        print("MyProtocol's testFuncA")
    }
}

class MyClass : MyProtocol {}

let object: MyClass = MyClass()
object.testFuncA()

静态调度被使用。 object 的具体类型在编译时是已知的;这是MyClass。然后 Swift 可以看到它符合 MyProtocol,而无需提供自己的 testFuncA() 实现,因此它可以直接调度到扩展方法。

所以回答您的个人问题:

1) 是否将扩展的默认实现复制到MyClass's v-table,以及通过MyClass的v-table调度的方法?

否 - Swift 类 v-table 仅包含在类声明主体中定义的方法。也就是说:

protocol MyProtocol {
  func testFuncA()
}

extension MyProtocol {
  // No entry in MyClass' Swift v-table.
  // (but an entry in MyClass' protocol witness table for conformance to MyProtocol)
  func testFuncA() {
    print("MyProtocol's testFuncA")
  }
}

class MyClass : MyProtocol {
  // An entry in MyClass' Swift v-table.
  func foo() {}
}

extension MyClass {
  // No entry in MyClass' Swift v-table (this is why you can't override
  // extension methods without using Obj-C message dispatch).
  func bar() {}
}

2) 是否仍然使用 Existential 容器来调度方法,以及 现有容器的 PWT 包含扩展的默认值 实施?

代码中没有存在容器:

let object: MyClass = MyClass()
object.testFuncA()

现有容器用于协议类型的实例,例如您的第一个示例:

let object: MyProtocol = MyClass()
object.testFuncA()

MyClass 实例被装在一个存在容器中,该容器带有一个协议见证表,该表将对 testFuncA() 的调用映射到扩展方法(现在我们正在处理动态调度)。


查看上述所有内容的一个好方法是查看编译器生成的 SIL;这是生成的代码的一个相当高级的中间表示(但足够低级,可以看出正在使用哪种调度机制)。

您可以通过运行以下命令来做到这一点(请注意,最好先从您的程序中删除 print 语句,因为它们会大大增加生成的 SIL 的大小):

swiftc -emit-sil main.swift | xcrun swift-demangle > main.silgen  

让我们看一下此答案中第一个示例的 SIL。这是main 函数,它是程序的入口点:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.object : main.MyClass       // id: %2
  %3 = global_addr @main.object : main.MyClass : $*MyClass // users: %9, %7

  // function_ref MyClass.__allocating_init()
  %4 = function_ref @main.MyClass.__allocating_init() -> main.MyClass : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // user: %6
  %5 = metatype $@thick MyClass.Type              // user: %6
  %6 = apply %4(%5) : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // user: %7
  store %6 to %3 : $*MyClass                      // id: %7

  // Get a reference to the extension method and call it (static dispatch).
  // function_ref MyProtocol.testFuncA()
  %8 = function_ref @(extension in main):main.MyProtocol.testFuncA() -> () : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // user: %12
  %9 = load %3 : $*MyClass                        // user: %11
  %10 = alloc_stack $MyClass                      // users: %11, %13, %12
  store %9 to %10 : $*MyClass                     // id: %11
  %12 = apply %8<MyClass>(%10) : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> ()
  dealloc_stack %10 : $*MyClass                   // id: %13


  %14 = integer_literal $Builtin.Int32, 0         // user: %15
  %15 = struct $Int32 (%14 : $Builtin.Int32)      // user: %16
  return %15 : $Int32                             // id: %16
} // end sil function 'main'

我们在这里感兴趣的是这一行:

  %8 = function_ref @(extension in main):main.MyProtocol.testFuncA() -> () : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // user: %12

function_ref 指令获取对函数的引用在编译时已知。你可以看到它得到了对函数@(extension in main):main.MyProtocol.testFuncA() -&gt; ()的引用,这是协议扩展中的方法。因此 Swift 使用的是静态调度。

现在让我们看看当我们这样调用时会发生什么:

let object: MyProtocol = MyClass()
object.testFuncA()

main 函数现在看起来像这样:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.object : main.MyProtocol  // id: %2
  %3 = global_addr @main.object : main.MyProtocol : $*MyProtocol // users: %9, %4
  // Create an opaque existential container and get its address (%4).
  %4 = init_existential_addr %3 : $*MyProtocol, $MyClass // user: %8
  // function_ref MyClass.__allocating_init()
  %5 = function_ref @main.MyClass.__allocating_init() -> main.MyClass : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // user: %7
  %6 = metatype $@thick MyClass.Type              // user: %7
  %7 = apply %5(%6) : $@convention(method) (@thick MyClass.Type) -> @owned MyClass // user: %8

  // Store the MyClass instance in the existential container.
  store %7 to %4 : $*MyClass                      // id: %8

  // Open the existential container to get a pointer to the MyClass instance.
  %9 = open_existential_addr immutable_access %3 : $*MyProtocol to $*@opened("F199B87A-06BA-11E8-A29C-DCA9047B1400") MyProtocol // users: %11, %11, %10

 // Dynamically lookup the function to call for the testFuncA requirement. 
  %10 = witness_method $@opened("F199B87A-06BA-11E8-A29C-DCA9047B1400") MyProtocol, #MyProtocol.testFuncA!1 : <Self where Self : MyProtocol> (Self) -> () -> (), %9 : $*@opened("F199B87A-06BA-11E8-A29C-DCA9047B1400") MyProtocol : $@convention(witness_method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // type-defs: %9; user: %11

  // Call the function we looked-up for the testFuncA requirement.
  %11 = apply %10<@opened("F199B87A-06BA-11E8-A29C-DCA9047B1400") MyProtocol>(%9) : $@convention(witness_method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // type-defs: %9


  %12 = integer_literal $Builtin.Int32, 0         // user: %13
  %13 = struct $Int32 (%12 : $Builtin.Int32)      // user: %14
  return %13 : $Int32                             // id: %14
} // end sil function 'main'

这里有一些关键的区别。

使用init_existential_addr 创建一个(不透明的)存在容器,并将MyClass 实例存储在其中(store %7 to %4)。

然后用open_existential_addr打开存在容器,它获取指向存储的实例的指针(MyClass 实例)。

然后,witness_method 用于查找函数以调用 MyProtocol.testFuncA 的协议要求 MyClass 实例。这将检查协议见证表中MyClass 的一致性,它列在生成的 SIL 的底部:

sil_witness_table hidden MyClass: MyProtocol module main {
  method #MyProtocol.testFuncA!1: <Self where Self : MyProtocol> (Self) -> () -> () : @protocol witness for main.MyProtocol.testFuncA() -> () in conformance main.MyClass : main.MyProtocol in main // protocol witness for MyProtocol.testFuncA() in conformance MyClass
}

这列出了函数@protocol witness for main.MyProtocol.testFuncA() -&gt; ()。我们可以检查这个函数的实现:

// protocol witness for MyProtocol.testFuncA() in conformance MyClass
sil private [transparent] [thunk] @protocol witness for main.MyProtocol.testFuncA() -> () in conformance main.MyClass : main.MyProtocol in main : $@convention(witness_method) (@in_guaranteed MyClass) -> () {
// %0                                             // user: %2
bb0(%0 : $*MyClass):
  %1 = alloc_stack $MyClass                       // users: %7, %6, %4, %2
  copy_addr %0 to [initialization] %1 : $*MyClass // id: %2

  // Get a reference to the extension method and call it.
  // function_ref MyProtocol.testFuncA()
  %3 = function_ref @(extension in main):main.MyProtocol.testFuncA() -> () : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // user: %4
  %4 = apply %3<MyClass>(%1) : $@convention(method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> ()


  %5 = tuple ()                                   // user: %8
  destroy_addr %1 : $*MyClass                     // id: %6
  dealloc_stack %1 : $*MyClass                    // id: %7
  return %5 : $()                                 // id: %8
} // end sil function 'protocol witness for main.MyProtocol.testFuncA() -> () in conformance main.MyClass : main.MyProtocol in main'

果然,它为扩展方法获取function_ref,并调用该函数。

然后在witness_method 查找之后调用查找到的见证函数:

  %11 = apply %10<@opened("F199B87A-06BA-11E8-A29C-DCA9047B1400") MyProtocol>(%9) : $@convention(witness_method) <τ_0_0 where τ_0_0 : MyProtocol> (@in_guaranteed τ_0_0) -> () // type-defs: %9

所以,基于witness_method的使用,我们可以得出这里使用动态协议调度的结论。

这里有很多技术细节,我们只是轻而易举;随时使用the documentation 逐行查看SIL,找出每条指令的作用。我很乐意澄清您可能不确定的任何事情。

【讨论】:

  • 你太棒了!这正是我想知道的!非常感谢!
  • @Maize 乐于助人:)
【解决方案2】:

这是一个棘手的问题,因为我们正在讨论编译器实现的细节,而且它们可以随着 Swift 的每个新版本而改变(所以任何知识都可能很快过时)。

说起 Swift 3,我前段时间遇到过一篇文章:https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/。它实际上告诉我们

Swift 有两个可以声明方法的位置:在 类型的初始声明,并在扩展中。取决于 声明的类型,这将改变调度的执行方式。

class MyClass {
    func mainMethod() {} }

extension MyClass {
    func extensionMethod() {} }

在上面的例子中,mainMethod 将使用 table dispatch,并且 extensionMethod 将使用直接调度

它还包含表格:

根据该表,在协议扩展(所谓的默认实现)中声明的方法总是直接调度

我不能确定,但​​我相信在 Swift 4 中可能会发生同样的行为。

【讨论】:

  • 我也读过这篇文章。我相信在我的例子中,testFuncA 属于 Initial Declaration 的情况,所以它应该是表调度。我的问题是“餐桌调度”是什么意思?它是使用 MyClass 的 v-table,还是存在容器的 PWT?
  • 据我了解,它应该使用 PWT。但我会和 LLDB 一起去检查
  • 您的意思是在没有存在容器的情况下使用 PWT?我以为 PWT 总是带有存在容器╮(╯_╰)╭ 顺便问一下,你的猜测是怎么检查的?
【解决方案3】:

class MyClass 比较你的协议 MyProtocol。所以你的对象知道协议实现。就这样。还是我没听懂问题?

【讨论】:

  • 所以你的意思是直接派送?
猜你喜欢
  • 2015-11-30
  • 1970-01-01
  • 2021-03-04
  • 1970-01-01
  • 1970-01-01
  • 2011-01-22
  • 1970-01-01
  • 2011-02-15
相关资源
最近更新 更多