Function distribution mode

A dispatch method that can be determined at compile time is called Static dispatch. A dispatch method that cannot be determined at compile time but only at run time is called Dynamic dispatch.

Static dispatch

Static dispatch is faster, the CPU gets the address of the function directly and calls it, and Static dispatch can be further optimized for inline execution and higher performance.

useStatic dispatchInstead ofDynamic dispatchImprove performance

We know that Static dispatches are faster than Dynamic dispatches, and how to use Static dispatches as much as possible in development.

  • Inheritance constraints

    We can use the final keyword to decorate the Class to generate a final Class, using Static dispatch.

  • Access control

    A private keyword modifier that makes a method or property visible only to the current class. The compiler performs Static dispatch on the method.

The compiler can check inheritance through Whole Module Optimization, evaluate certain classes that are not marked final, and use Static Dispatch if it can determine the method to execute at compile time. Struct uses Static dispatch by default.

Swift provides more flexible structs for performance optimization in terms of memory, reference counting, method dispatch, etc. Choosing the right data structure at the right time can make our code performance faster and safer.

You might ask how does a Struct implement polymorphism? The answer is Protocol Oriented programming.

How do different algorithmic mechanisms Class, Protocol Types, and Generic code perform in these three aspects? How are Protocol Types and Generic code implemented respectively? So let’s go with that.

Protocol Type

Here we discuss how Protocol Types store and copy variables, and how method dispatch is implemented. Polymorphisms without inheritance or reference semantics:

Struct Point :Drawable {var x, y:Double draw() {struct Point :Drawable {var x, y:Double draw() {... }} struct Line :Drawable {var x1, y1, y2:Double draw() {struct Line :Drawable {var x1, y1, y2:Double draw() {... Drawables {d.drawables ()} var drawables:[Drawable] {d.drawables ()} var drawables:[Drawable]Copy the code

Because of the different sizes of Point and Line, array storage implements consistent storage, using An Interface Container. Protoloc Witness Table is used to find the correct execution method.

The above is realized through Protocol Type polymorphism. There is no inheritance relationship between several classes, so dynamic dispatch cannot be realized by using V-table as usual. However, for Swift, the implementation of the class class is different from that of the struct structure, and the Protocol Protocol that belongs to the structure can have attributes and implementation methods. The Table that manages the assignment of the Protocol Type method is called Protocol Witness Table.

Protocol Witness Table

Like V-Table,Protocol Witness Table(PWT for short) stores an array of methods, which contains the pointer address of the method implementation. When invoking a method, the method is searched by obtaining the memory address of the object and the offset of the method.

The Protocol Witness Table is used to manage Protocol Type method calls. When we get to swift performance tuning, we hear another concept called Value Witness Table (VWT for short).

Value Witness Table

Manages initialization, copying, and destruction of arbitrary values. That is, the lifecycle of the Protocol Type is managed specifically

For each type (Int or custom), metadata stores a VWT that manages the value of the current type

Value Witness Table and Protocol The Witness Table manages memory management (initialization, copy, and destruction) and method calls of Protocol Type instances.

Dynamic dispatch

However, in the case of polymorphism, we cannot determine the final type at compile time, so Dynamic dispatch is used. The implementation of dynamic dispatch is that each type creates a table containing an array of method Pointers. Dynamic dispatch is more flexible, but because there are table lookups and jumps, and because many features are not clear to the compiler, it is equivalent to some late optimization by the compiler. So it’s slower than Static Dispatch.

Table Dispatch

Distribution, the most common method in compiled languages, ensures both dynamic and efficient execution.

The class of a function maintains a “function table” (virtual function table) that accesses Pointers to each function implementation.

The vtable for each class is built at compile time, so there are two more reads than static distribution:

  • Read the Vtable of the class
  • Read a pointer to a function

Advantages:

  • Table lookup is simple, easy to implement, and predictable.
  • Theoretically, function table distribution is also an efficient way.

Disadvantages:

  • Compared to static dispatch, there are two more reads and one more hop from a bytecode perspective.
  • The compiler cannot optimize certain functions with side effects compared to static distribution.
  • Functions in the Swift class extension cannot be dynamically added to the function table of the class, and can only be distributed statically.

Here’s an example (just an example) :

class A {
    func method1() {}
}
class B: A {
    func method2() {}
}
class C: B {
    override func method2() {}
    func method3() {}
}
Copy the code


Copy the code
offset 0xA00 A 0xB00 B 0xC00 C
0 0x121 A.method1 0x121 A.method1 0x121 A.method1
1 0x222 B.method2 0x322 C.method2
2 0x323 C.method3
let obj = C()
obj.method2()
Copy the code

When method2 is called, the following steps occur:

  1. Read the object0xC00The function table
  2. Read the index of the function pointer,method2The address for0x322
  3. Jump to perform0x322

Message Dispatch

If you are familiar with OC, you will know that OC uses the runtime mechanism to send messages using obj_msgSend. Runtime is very flexible. We can not only use swizzling for method calls, but also extend the functionality of objects through ISa-Swizzling. The application scenarios include hook and KVO.

Everyone asks when developing with Swift, can Swift use OC’s runtime and message forwarding mechanisms? The answer is yes.

Swift can mark a method with the keyword Dynamic, which tells the compiler that the method uses OC’s run-time mechanism.

id returnValue = [obj messageName:param]; Id returnValue = objc_msgSend(obj, @selector(messageName:), param); Copy the codeCopy the code

Advantages:

  • High dynamic
  • Method Swizzling
  • isa Swizzling
  • .

Disadvantages:

  • The execution efficiency is the lowest among the three distribution methods

Fortunately, objc_msgSend caches the matching results in a mapping table, which every class has. If the same message is sent later, the execution rate is very fast.

To sum up, Swift includes three dispatch methods after the extension of dynamic keyword: Static Dispatch, Table Dispatch and Message Dispatch. The following table shows how different data structures dispatch in different situations:

type Static distributed Function table distribution Messages distributed
Value types All the methods / /
agreement extension The main body to create /
class extension/final/static The main body to create @objc + dynamic
NSObject subclass extension/final/static The main body to create @objc + dynamic

If you mix these dispatch methods incorrectly during development, there may be bugs, which are analyzed below:

This is the case when a parent class method is overridden in a subclass’s Extension and behaves differently than expected.

class Base:NSObject {
    var directProperty:String { return "This is Base" }
    var indirectProperty:String { return directProperty }
}

class Sub:Base { }

extension Sub {
    override var directProperty:String { return "This is Sub" }
}
Copy the code

Execute the following code, directly call no problem:

Base().directProperty // "This is Base" Sub().directProperty // "This is Sub"Copy the code

Indirect invocation results in different results than expected:

Base().indirectProperty // "This is Base" Sub().indirectProperty // Expected "This is Sub", But is "This is Base" < -unexpected!Copy the code

Add the dynamic keyword to base.directProperty to get the result of “this is Sub”. Swift states in the Extension documentation that you cannot overload existing methods in Extension.

“Extensions can add new functionality to a type, but they cannot override existing functionality.”

Cannot override a non-dynamic class declaration from an extension.

The reason for this problem is that NSObject’s Extension uses Message Dispatch, whereas Initial Declaration uses Table Dispath (see figure above). Extension overloaded methods are added to Message Dispatch. The virtual table is not modified. The virtual table still contains methods of the parent class, so the parent class methods are executed. To reload methods in Extension, you need to indicate Dynamic to use Message Dispatch.

A method implemented in an extension of a protocol that cannot be subclassed by class overloading:

Protocol Greetable {func sayHi()} Extension Greetable {func sayHi() {print("Hello")}} Func greetings(greeter: greeter) Greetable) { greeter.sayHi() }Copy the code

Now define a class Person that complies with the protocol. Subclass of LoudPerson:

class Person:Greetable {
}
class LoudPerson:Person {
    func sayHi() {
        print("sub")
    }
}
Copy the code

Executing the following code results in:

var sub:LoudPerson = LoudPerson()
sub.sayHi()  //sub
Copy the code

Code that does not meet expectations:

Var sub:Person = LoudPerson() sub.sayhi () //HellO <- Uses the default implementation of protocolCopy the code

Notice that the override keyword does not appear in subclass LoudPerson. LoudPerson did not successfully register Greetable in the Witness table method. Therefore, for instance declared as Person but actually LoudPerson, the compiler uses Person to search for instance. If Person does not implement the protocol method, Witness table is not generated and the sayHi method is directly invoked. The solution is to implement the protocol methods in the Base class and provide the default methods without the implementation. Or mark the base class as final to avoid inheritance.

To understand this further, use an example:

/ / Defined protocol. protocol A { func a() -> Int } extension A { func a() -> Int { return 0 } } // A class doesn't have implement of the The function. Class B: A {} class C: B {func A () -> Int {return 1}} // A class has this implement. Class D: A {func A () -> Int {return 1}} Class E: D {override func A () -> Int {return 2}} // Failure cases. (B) (a) / / 0 C () () a / / 1. (C) (as a) a () / / 0 # We thought the return 1. / / Success cases. D().a() // 1 (D() as A).a() // 1 E().a() // 2 (E() as A).a() // 2Copy the code

If you don’t understand the results of the above code, you can also use the meowgod PROTOCOL EXTENSION example to understand:

We can now extend an existing protocol, and the methods implemented in the extension will be the default implementation of the type that implements the extension. That is, suppose we have the following protocol declaration and an extension to the interface:

protocol MyProtocol {
    func method()
}

extension MyProtocol {
    func method() {
        print("Called")
    }
}
Copy the code

In a specific type that implements this interface, we can compile it even if we write nothing. To make the call, you use the implementation in Extension directly:

Struct MyStruct: MyProtocol {} MyStruct().method() // output: // Called in extensionCopy the code

Of course, if we need other implementations in the type, we can add this method to the concrete type as before:

struct MyStruct: MyProtocol {func method() {print("Called in struct")}} MyStruct(). Method (Copy the code

That is, protocol Extension provides a default implementation for methods defined in Protocol. With this feature, the map method that receives the ectionType, previously placed in the global environment, can be moved to the interface extension of the ectionType:

extension CollectionType {
    public func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
    //...
}
Copy the code

Another place you can use Protocol Extension in everyday development is the Optional interface methods. By providing protocol’s extension, we provide a default implementation for Protocol, which is equivalent to making the methods in Protocol optional. We have covered this in the optional interface and interface Extension section and will not repeat it.

One of the most confusing situations with Protocol Extension is when an extension to an interface implements methods that are not defined in the interface. For example, let’s define an interface like this and an extension to it:

protocol A1 {
    func method1() -> String
}

struct B1: A1 {
    func method1() -> String {
        return "hello"
    }
}
Copy the code

When used, whether we call the instance of type A1 or B1, since there is only one implementation, there is no question that the output when the method is called is “hello” :

let b1 = B1() // b1 is B1
b1.method1()
// hello

let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
Copy the code

But if only one method is defined in the interface, and additional methods are implemented in the interface extension, things get interesting. Consider the following set of interfaces and their extensions:

protocol A2 {
    func method1() -> String
}

extension A2 {
    func method1() -> String {
        return "hi"
    }

    func method2() -> String {
        return "hi"
    }
}
Copy the code

In addition to implementing method1 for the interface definition, the extension defines a method, method2, that does not exist in the interface. We tried to implement this interface:

struct B2: A2 {
    func method1() -> String {
        return "hello"
    }

    func method2() -> String {
        return "hello"
    }
}
Copy the code

Method1 and method2 are implemented in B2. Next, we try to initialize a B2 object and call both methods:

let b2 = B2()

b2.method1() // hello
b2.method2() // hello
Copy the code

The result is not surprising. Although these two methods are implemented in Protocol Extension, they are only default implementations, and it makes sense that we can override the default implementation in the type of interface we implement. But if we change it a bit and continue after the code above:

let a2 = b2 as A2

a2.method1() // hello
a2.method2() // hi
Copy the code

A2 and B2 are the same object, except we use AS to tell the compiler that the type we need here is A2. But what happens when you call the same method on the same object and you get a different result?

As you can see, calling method2 on A2 is actually a method being called in the interface extension, not in the A2 instance. Let’s think about it this way: For method1, because it is defined in protocol, it is safe to assume that an instance of a type that is declared to comply with the interface (that is, for A2) must implement method1, We can safely use the final implementation (whether it is a concrete implementation in a type or a default implementation in an interface extension) in a dynamically distributed manner. But with Method2, we just defined it in the interface extension, and there’s nothing that says it has to be implemented in the final type. When used, because A2 is just an instance that conforms to the A2 interface, the only thing the compiler can be sure of with Method2 is that there is a default implementation in the interface extension, so when called, it is not safe to dynamically distribute and instead falls back on the default implementation determined at compile time.

In this case, you might be okay with that, because it’s unlikely that anyone would actually cast an instance of a known type back to the interface type. However, if some of your generic apis have similar methods that directly fetch the result of an interface type, you need to be very careful about calling its extension methods: In general, if there is a need, we can consider calling the interface type back to the actual type.

To sort out the rules:

  • If the type inference is the actual type

    • Then the implementation in the type will be called; If there is no implementation in the type, the default implementation in the interface extension will be used
  • If type inference results in an interface rather than the actual type

    • And the method is defined in the interface, then the implementation in the type will be called; If there is no implementation in the type, the default implementation in the interface extension is used

    • Otherwise (that is, the method is not defined in the interface), the default implementation in the extension will be called

Reference:

PROTOCOL EXTENSION

[Basic skills] In-depth analysis of Swift performance optimization

The distribution mechanism of Swift function from SIL