Distributing mechanism

Before exploring the Swift distribution mechanism, we should first understand the basic knowledge of function distribution. Function dispatch is the mechanism by which a program determines which way to call a function. It is the process by which the CPU finds the address of the function in memory and calls it. Something that gets fired every time a function is called, but that you don’t pay much attention to. Understanding function distribution is essential to writing high-performance code, and can explain a lot of the “strange” behavior in Swift.

Compiled languages have three basic ways of distributing functions:

  • Direct Dispatch (also known as static Dispatch)
  • Table Dispatch
  • Message Dispatch.

Most languages support one or two. Java uses table distribution by default, but you can change it to direct distribution using the final modifier. C++ uses direct distribution by default, but can be changed to table distribution by adding the virtual modifier. Objective-c, on the other hand, always uses messaging, but allows developers to use C to deliver directly for performance gains.

Direct Dispatch

Direct distribution is the fastest of the three. The CPU calls directly according to the function address, using the fewest instruction set, to do the fastest thing. When a compiler optimizes a program, it often inlines functions to make them distributed directly, optimizing execution speed. The familiar C++ default uses direct distribution. Add the final keyword to a function in Swift and the function becomes direct distribution. Of course, there are advantages and disadvantages. The biggest disadvantage of direct distribution is that it is not dynamic and does not support inheritance.

Table Dispatch

This is the most common distribution method for compiled languages, which ensures both dynamic and efficient execution. The class in which a function resides maintains a “function table,” also known as a virtual function table, known in Swift as a “Witness table.” The function table accesses Pointers to each function implementation. The Vtable for each class is built at compile time, so there are only two more reads: the vtable for the class and the pointer to the function. Theoretically, function table distribution is also an efficient way. However, the compiler is unable to optimize some functions with side effects compared to direct distribution, which also causes the slower distribution of function tables. Moreover, the methods in the Swift class extension cannot be dynamically added to the function table of the class, and can only be distributed statically, which is also one of the defects of the function table distribution.

Let’s look at the following code:

class ParentClass {
    func method1() {}
    func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    func method3() {}}Copy the code

In the current scenario, the compiler creates two function tables: one for the ParentClass class and one for the ChildClass class, with the following memory layout:

let obj = ChildClass()
obj.method2()

Copy the code

When the function method2() is called, the process is as follows:

Read the vtable of this object (0XB00). Read the method2 function pointer 0x222. Jump to address 0X222, read function implementation.Copy the code

Table lookup is simple, easy to implement, and predictable. However, this method of distribution is slower than direct distribution. From a bytecode perspective, there are two more reads and one more jump, resulting in a performance penalty. Another reason for the slowness is that, as we mentioned earlier, the compiler’s inability to optimize certain functions with side effects is also one of the reasons for the slowness of the function table distribution. The drawback of this array-based implementation is that the function table cannot be expanded. Subclasses insert new functions at the end of the imaginary function list, and there is no place where Extension can safely insert functions.

Message Dispatch

The messaging mechanism is the most dynamic way to invoke functions. It is the cornerstone of Cocoa, and this mechanism has spawned features like KVO, UIAppearence, and CoreData. The key to this approach is that the developer can change the behavior of the function at run time. Not only can swizzling be changed, but also isA-Swizzling can be used to modify the inheritance relationship of the object, and customized distribution can be realized on the basis of object-oriented.

Since Swfit is still using Objc’s runtime system, Message delivery here is really Objc’s Message Passing.

id returnValue = [someObject messageName:parameter];
Copy the code

SomeObject is the receiver, messageName is the selector, and the selector and parameter together are called “messages”. When compiled, the compiler converts the message into a standard C call:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

Copy the code

The objc_msgSend function calls the appropriate method with a receiver and selector type, searches the list of methods in the recipient’s class, and jumps to the corresponding implementation if it finds one. If it cannot be found, continue to search upward along the inheritance system. If it can be found, jump. If it doesn’t, perform a boundary case operation, such as Message Forwarding. Look at the following code:

Swift uses trees to build this inheritance:

class ParentClass {
    dynamic func method1() {}
    dynamic func method2() {}
}
class ChildClass: ParentClass {
    override func method2() {}
    dynamic func method3() {}}Copy the code

This seems like a lot of steps, but the good news is that objc_msgSend caches matching results in a fast map, and each class has one. If the same message is later sent, the execution rate is very fast, increasing the performance to as fast as the function table dispatch.

How does Swift distribute functions

With the basics of function dispatching behind us, let’s take a look at how Swift handles function dispatching and how to prove it. Let’s start with a summary:

From the above table, we can intuitively conclude that the distribution mode of functions is related to the following two points:

Object type; Value types always use the location of the function declaration directly (statically, because they have no inheritance); Declare directly in the definition and in the extensionCopy the code

In addition, specifying the distribution mode explicitly changes the distribution mode of the function, such as adding the final or @objc keyword. And compiler optimizations for specific functions, such as optimizing private functions that have never been overridden for static distribution.

Below, we analyze and discuss the distribution mode of Swift from these four aspects, and prove its distribution mode.

Object type

As mentioned above, objects of value types, namely structs, are always distributed statically; Class objects are distributed using function tables (non-extension). See the following code example:

class MyClass {
    func testOfClass() {}
   
}

struct MyStruct{
    func testOfStruct() {}}Copy the code

Now we convert the swift code to SIL(intermediate code) using the following command to see how its function is distributed:

swiftc -emit-silgen -O main.swift
Copy the code

The following output is displayed:

. class MyClass { functestOfClass() @objc deinit // add init() // add} struct MyStruct {functestSil_vtable sil_vtable MyClass {#MyClass.testOfClass! 1: (MyClass) -> () -> () : @$s4main7MyClassC06testOfC0yyF // MyClass.testOfClass()
  #MyClass.init! allocator.1: (MyClass.Type) -> () -> MyClass : @$s4main7MyClassCACycfC // MyClass.__allocating_init()
  #MyClass.deinit! deallocator.1: @$s4main7MyClassCfD // MyClass.__deallocating_deinit
}

Copy the code

First swift adds init and @objc deinit methods to the class and init methods to the struct. The code above is displayed at the end of the file, showing which functions are distributed from the function table, along with their identifiers. Since the struct type is issued statically only, sil_vtable is not displayed.

Function declaration position

The position of function declaration can also lead to different distribution methods. In Swift, we often add extension methods in extension. According to the table we summarized earlier, normally functions declared in Extension are statically distributed by default.


protocol MyProtocol {
    func testOfProtocol()
}

extension MyProtocol {
    func testOfProtocolInExtension() {}
}

class MyClass: MyProtocol {
    func testOfClass() {}
    func testOfProtocol() {}
}

extension MyClass {
    func testOfClassInExtension() {}}Copy the code

We declare a function in Protocol and class, and a function in extension. Finally, let the class implement a method of the protocol, converted to SIL code as follows:

protocol MyProtocol {
  func testOfProtocol()
}

extension MyProtocol {
  func testOfProtocolInExtension()
}

class MyClass : MyProtocol {
  func testOfClass()
  func testOfProtocol()
  @objc deinit
  init()
}

extension MyClass {
  func testOfClassInExtension()
}

...

///sil_vtable
sil_vtable MyClass {
  #MyClass.testOfClass! 1: (MyClass) -> () -> () : @$s4main7MyClassC06testOfC0yyF // MyClass.testOfClass()
  #MyClass.testOfProtocol! 1: (MyClass) -> () -> () : @$s4main7MyClassC14testOfProtocolyyF // MyClass.testOfProtocol()
  #MyClass.init! allocator.1: (MyClass.Type) -> () -> MyClass : @$s4main7MyClassCACycfC // MyClass.__allocating_init()
  #MyClass.deinit! deallocator.1: @$s4main7MyClassCfD // MyClass.__deallocating_deinit
}

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


Copy the code

Functions declared in a protocol or class body are distributed using a function table. Functions declared in extensions are statically dispatched.

It is important to note that when we declare a function in Protocol and implement it in Protocol extension, and no other type overwrites the function, in this case the function is distributed directly and is considered generic.

Specify the distributing

Adding keywords to a function can also change the way it is distributed.

final

Functions with the final keyword cannot be overwritten and will not appear in the Vtable using direct distribution. And not visible to the Objc Runtime

dynamic

The dynamic keyword can be added to functions of both value and reference types. In Swift5, the purpose of adding dynamic to functions is to make non-objC classes and value types (struct and enum) dynamic. Let’s look at the following code:

struct Test {
    dynamic func test() {}}Copy the code

We made the test function dynamic. It is converted to SIL intermediate code as follows:

// Test.test()
sil hidden [dynamically_replacable] [ossa] @$s4main4TestV4testyyF : $@convention(method) (Test) -> () {
// %0                                             // user: %1
bb0(%0 : $Test):
  debug_value %0 : $Test.let, name "self", argno 1 // id: %1
  %2 = tuple ()                                   // user: %3
  return %2 : $()                                 // id: %3
} // end sil function '$s4main4TestV4testyyF'

Copy the code

We can see in the second line that the test function has an additional “property” : dynamically_replacable, meaning that adding the dynamic keyword gives the function the ability to replace dynamically. So what is dynamic substitution? In short, provide A way to, for example, dynamically replace methods defined in Module A in Module B, as shown below:

struct ModuleAStruct {

    dynamic func testModuleAStruct() {print("struct-testModuleAStruct")
    }
}



extension ModuleAStruct{
    @_dynamicReplacement(for: testModuleAStruct())
    func testModuleAStructReplacement() {
        print("extension-testModuleAStructReplacement")}}letFoo = ModuleAStruct () foo testModuleAStruct () / / / by calling the test print is the extension - testModuleAStructReplacementCopy the code

Note: Adding the dynamic keyword does not make Objc visible.

@objc

This keyword exposes the Swift function to the Objc runtime, but does not change how it is distributed, which is still a table of functions. Examples are as follows:

class Test {
    @objc func test() {}}Copy the code

The SIL code is as follows:

. // @objc Test.test() sil hidden [thunk] [ossa] @$s4main4TestC4testyyFTo : $@convention(objc_method) (Test) -> () {
// %0                                             // user: %1
bb0(%0 : @unowned $Test):
  %1 = copy_value %0 : $Test                      // users: %6, %2
  %2 = begin_borrow %1 : $Test                    // users: %5, %4
  // function_ref Test.test()
  %3 = function_ref @$s4main4TestC4testyyF : $@convention(method) (@guaranteed Test) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@guaranteed Test) -> () // user: %7
  end_borrow %2 : $Test                           // id: %5
  destroy_value %1 : $Test                        // id: %6
  return %4 : $()                                 // id: %7
} // end sil function '$s4main4TestC4testyyFTo'. sil_vtable Test {#Test.test! 1: (Test) -> () -> () : @$s4main4TestC4testyyF // Test.test()
  #Test.init! allocator.1: (Test.Type) -> () -> Test : @$s4main4TestCACycfC // Test.__allocating_init()
  #Test.deinit! deallocator.1: @$s4main4TestCfD // Test.__deallocating_deinit
}

Copy the code

We can see that the test method is still in the “virtual function list”, proving that the function table is actually dispatched. If you want the test function to use message distribution, you need to add the additional dynamic keyword.

@inline or static

The @inline keyword, as its name implies, tells the compiler to dispatch this function directly, but after it is converted to SIL code, it is still distributed as vtable. The Static keyword changes the function to direct dispatch.

Compiler optimization

Swift optimizes the distribution of functions as much as possible. As we mentioned earlier, when a class declares a private function, that function is likely to be optimized for direct distribution.

Distributed to summarize

Finally, we use a diagram to summarize the distribution mode in Swift:

As can be seen from the above table, most of the functions declared in the body of the type are function table distribution, which is also the most common distribution mode in Swift. Extensions are mostly distributed directly; Function distribution changes only when certain keywords are added, such as @objc, final, and dynamic. In addition, the compiler may optimize some methods for direct distribution. For example, private functions.