• Optimization Tips
  • Original author: Apple
  • The Nuggets translation Project
  • Translator: joyking7
  • Proofread by NathanWhy, Walkingway

This article incorporates many tips and tricks for writing high-performance Swift code. This article is intended for compiler and library developers.

Some of the tips in this article can help improve the quality of your Swift program and reduce error-prone code, making it more readable. A protocol that explicitly marks the final class and class are two obvious examples. However, some of the techniques described in this article are nonconforming, distorted, and only solve problems temporarily limited by the compiler or language. The recommendations in this article come from trade-offs such as program runtime, binary size, code readability, and so on.

To enable optimized

The first thing everyone should do is enable optimizations. Swift offers three different optimization levels:

  • -Onone: This is for normal development, it performs minimal optimizations and retains all debugging information.
  • -O: is intended for most production code, and the compiler performs aggressive optimizations that can greatly change the type and amount of submitted code. Debugging information is also output, but with loss.
  • -Ounchecked: This is a specific optimization pattern that compromises security to improve performance for a particular library or application. The compiler removes all overflow checking as well as some implicit type checking. This pattern is generally not used because it can lead to undetected storage security issues and integer overflows. Only use if you have carefully reviewed your code to be integer overflow and type conversion friendly.

In Xcode UI, one can modify the current optimization level as follows:

Tuning the entire component

By default, Swift compiles each file individually. This allows Xcode to compile multiple files in parallel very quickly. However, compiling each file separately can prevent some compiler optimizations. Swift can also compile the entire program as a file and optimize the program as a single compilation unit. This pattern can be enabled using the command line -whole-module-optimization. Programs take longer to compile in this mode, but run faster.

This pattern can be enabled through Whole Module Optimization in the Xcode build Settings.

Reducing Dynamic Dispatch

By default, Swift is a dynamic language like Objective-C. Unlike Objective-C, programmers can remove or reduce Swift’s dynamic features when necessary to improve runtime performance. This section provides several examples that can be used to manipulate language constructs.

Dynamic scheduling

By default, classes use dynamically scheduled method and property access. So in the following snippet, a.property, a.dosomething () and a.dosomethingelse () will all be called by dynamic scheduling:

  class A {
    var aProperty: [Int]
    func doSomething() { ... }
    dynamic doSomethingElse() { ... }
  }

  class B : A {
    override var aProperty {
      get { ... }
      set { ... }
    }

    override func doSomething() { ... }
  }

  func usingAnA(a: A) {
    a.doSomething()
    a.aProperty = ...
  }
Copy the code

In Swift, dynamic scheduling is invoked indirectly by default through a VTable 1 (virtual function table). If declared with the dynamic keyword, Swift will be called: “Via Objective-C messaging mechanism.” In both cases, the latter “messaging through Objective-C” is slower than direct function calls because it prevents many optimizations by the compiler, 2 in addition to its own overhead of indirect calls. In performance-first code, people often want to limit this dynamic behavior.

Suggestion: Use it when you know it won’t be overridden at the time you declare itfinal

The final keyword is a restriction ina class, method, or property declaration that prevents the declaration from being overridden. This means that the compiler can use direct function calls instead of indirect ones. For example, c. array1 and D.array1 below will be accessed directly 3. Instead, d. array2 will be accessed through a virtual function table:

Var array1: [Int] func doSomething() {... }} class D {final var array1 [Int] // var array2: [Int] //'array2'* can * be overridden} func usingC(c: C) { c.array1[i] = ... // you can use c.ray directly instead of dynamically calling c.dosomething () =... Func usingD(d: d) {d.array1[I] =... func usingD(d: d) {d.array1[I] =... // You can use d.array1 directly instead of calling d.array2[I] =... // will use d.array2} through dynamic callsCopy the code

Suggestion: Use this parameter when the declaration does not need to be accessed externallyprivate

Using the private keyword in a declaration limits visibility to its declaration file. This allows the compiler to detect all other potential overrides. Therefore, without such a declaration, the compiler can automatically infer the final keyword and remove indirect method calls and domain access. For example, assuming E, F do not have any rewrite declarations in the same file, then e.dosomething () and f.myprivatevar will be directly accessible:

private class E { func doSomething() { ... } } class F { private var myPrivateVar : Int } func usingE(e: E) {e.dosomething () // There is no alternative class in the file to declare this class // The compiler can remove the virtual call to doSomething() and call the doSomething method of class E directly} func usingF(f: F) -> Int { return f.myPrivateVar }Copy the code

Use container types efficiently

The generic containers Array and Dictionary are an important feature provided by the Swift standard library. This section explains how to use these types in a high-performance way.

Suggestion: Use value types in arrays

In Swift, types can be divided into two distinct categories: value types (structs, enumerations, tuples) and reference types (classes). One key difference is that NSArray cannot contain value types. So when using value types, the optimizer doesn’t have to deal with NSArray support, saving most of the overhead on arrays.

In addition, if a value type contains a reference type recursively compared to a reference type, then the value type only needs a reference counter. By using a value type that does not contain a reference type, you can avoid the extra overhead (the amount of traffic generated when elements in an array perform retain and release operations).

Struct PhonebookEntry {var name: String var number: [Int]} var a: [PhonebookEntry]Copy the code

Keep in mind the tradeoff between using large value types and reference types. In some cases, the cost of copying and moving large value types is greater than the cost of removing Bridges and retaining/releasing them.

Suggestion: If the NSArray bridge is unnecessary, use a ContiguousArray to store the reference type

If you need an Array of reference type, and that Array doesn’t need to be bribed to an NSArray, use a ContiguousArray instead of Array.

  class C { ... }
  var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]
Copy the code

Suggestion: Use in-place transformations rather than object redistribution

In Swift, all library containers are value types and copy is performed using the copy-on-write (COW) 4 mechanism instead of direct copy. In many cases, the compiler can save unnecessary copies by keeping a reference to the container rather than performing a deep copy. If the container’s reference count is greater than 1 and the container is transformed, this will only be done by copying the underlying container. For example, when D is assigned to C, no copy is made, but when D is appended to 2 by a structural change, then D is copied, and then 2 is appended to D:

var c: [Int] = [ ... ] Var d = c // there is no copy d.append(2Copy the code

Sometimes the COW mechanism causes extra copies if the user is not careful. For example, in a function, an attempt is made to perform a modification operation by reallocating an object. In Swift, all arguments are copied as they are passed; for example, arguments are held until the call is made, and then released at the end of the call. That is, functions like the following:

  func append_one(a: [Int]) -> [Int] {
    a.append(1)
    return a
  }

  var a = [1, 2, 3]
  a = append_one(a)
Copy the code

Although a (not initially append) is not used after append_one, 5 May still be copied. This can be avoided by using the parameter inout:

  func append_one_in_place(inout a: [Int]) {
    a.append(1)
  }

  var a = [1, 2, 3]
  append_one_in_place(&a)
Copy the code

Unchecked operation

When performing normal integer arithmetic, Swift checks for overflow to eliminate bugs. However, in high-performance code where no memory safety issues are known to occur, such checks are not appropriate.

Suggestion: Use unchecked integer computations if you know that overflow will not occur

In performance-first code, you can ignore overflow checking if you know the code is safe.

A: (Int) b: c: [Int] [Int] / / premise: for all [I] a, b [I], [I] a [I] + b will not overflow! for i in 0 ... n { c[i] = a[i] &+ b[i] }Copy the code

The generic

Swift provides a powerful abstraction mechanism through its use of generic types. The Swift compiler issues a concrete block of code to execute MySwiftFunc

on any T. The generated code requires a table of function Pointers and a wrapper containing T as additional arguments. The different behavior between MySwiftFunc

and MySwiftFunc

is illustrated by passing different function pointer tables and the abstract size provided by the wrapper. An example of generics:


class MySwiftFunc<T> { ... } MySwiftFunc<Int> X // will pass code MySwiftFunc<String> Y // here is a StringCopy the code

When optimization is enabled, the Swift compiler looks at each invoked piece of code and tries to figure out what type is being used (for example, non-generic types). If the generic function definition is visible to the optimizer and the specific type is known, the Swift compiler produces a special generic function with a special type. This process is called specialization and avoids the cost of generics association. Some examples of generics:

class MyStack<T> { func push(element: T) { ... } func pop() -> T { ... } } func myAlgorithm(a: [T], length: Int) { ... Var stackOfInts: MyStack[Int] // Use a stack of integer types for I in... { stack.push(...) stack.pop(...) } var arrayOfInts: [Int] // The compiler can execute a specialized version of myAlgorithm(arrayOfInts, arrayOfints.length) for the function that targets [Int].Copy the code

Suggestion: Put the generic declaration in the file that uses it

The optimizer can specialize only if the generic declaration is visible in the current module. This only happens if you use generics and declare them in the same file. Note that the standard library is an exception. Declaring generics in the standard library makes them visible and specialized to all modules.

Suggestion: Allow the compiler to do generic specialization

The compiler can specialize generic code only if the calling and called functions are in the same compilation unit. One trick we can use to get the compiler to optimize the called function is to execute type-checking code in the compilation unit of the called function. The type checking code is sent back to call the generic function – but doing so includes the type information. In the following code, we insert type checking in the function “play_a_game”, making the code run hundreds of times faster.

//Framework.swift: protocol Pingable { func ping() -> Self } protocol Playable { func play() } extension Int : Pingable { func ping() -> Int { return self + 1 } } class Game<T : Pingable> : Playable { var t : T init (_ v : T) {t = v} func play() { for _ in 0... 100_000_000 {t = t.ping()}}} func play_a_game(game: Playable) {// This check allows the optimizer to specialise the generic function 'play' if let z = game as? Game<Int> { z.play() } else { game.play() } } /// -------------- >8 // Application.swift: play_a_game(Game(10))Copy the code

Overhead of large value types in Swift

In Swift, a value retains a unique copy of the data. The use of value types has many advantages, such as ensuring that values have independent states. When we copy the value (equivalent to allocation, initialization, and parameter passing), the program creates a new copy. For large value types, such copying can be time-consuming and can affect program performance.

More about the knowledge of the value type: developer.apple.com/swift/blog/…

Consider the code that defines a tree using a node of type ‘value’. The nodes of the tree include other nodes that use the protocol. Computer graphics scenes are usually made up of different entities and morphologies that can be represented as values, so this example makes a lot of sense.

Check for protocol programming: developer.apple.com/videos/play…

protocol P {} struct Node : P { var left, right : P? } struct Tree { var node : P? init() { ... }}Copy the code

When a tree is copied (passing arguments, initializing or assigning operations), the entire tree is copied. This is an expensive operation, requiring a lot of malloc/free calls and a lot of reference counting.

However, we don’t really care if the values are copied as long as they remain in memory.

Suggestion: Use the copy-on-write mechanism for large value types

To reduce the overhead of copying large value types, you can use the copy-on-write method. The easiest way to implement copy-on-write is to use existing copy-on-write data structures, such as arrays. Swift’s array is a value type, and because of its copy-on-write nature, when an array is passed as a parameter, it does not need to be copied every time.

In our ‘tree’ example, we reduce copying overhead by encapsulating the contents of the tree into an array. With this simple change, which gives us a great indication of the tree’s data structure performance, the overhead of passing arrays as arguments is reduced from O(n) to O(1).

struct Tree : P { var node : [P?]  init() { node = [ thing ] } }Copy the code

Using arrays to implement COW has two obvious drawbacks. The first problem is methods like “append” and “count” in arrays, which have no role in value encapsulation. These methods make reference encapsulation very inconvenient. We can solve this problem by creating a wrapper structure that hides unused apis and the optimizer removes its overhead, but such a wrapper does not solve the second problem. The second problem is that there is code in the array to ensure program security and to interact with Objective-C. Swift checks to see if index access is within array boundaries, and when it saves the value, determines if the array needs to be extended. All of these operations slow down the program as they run.

An alternative is to implement a copy-on-write mechanism for data structures that encapsulate values instead of arrays. The following example shows how to build such a data structure:

Note: Such a solution is not optimal for nested structures, and an ADDRESSOR based on COW data structures would be more efficient. In this case, however, implementing AddresSOR without the standard library is not an option.

More details see Mike Ash’s blog: www.mikeash.com/pyblog/frid…

final class Ref<T> { var val : T init(_ v : T) {val = v} } struct Box<T> { var ref : Ref<T> init(_ x : T) { ref = Ref(x) } var value: T { get { return ref.val } set { if (! isUniquelyReferencedNonObjC(&ref)) { ref = Ref(newValue) return } ref.val = newValue } } }Copy the code

The Box type can replace the array in the previous example.

Unsafe code

Classes in Swift always use reference counting. The Swift compiler inserts code that increases the reference count each time an object is accessed. For example, consider an example of traversing a linked list using a class. The list is traversed by moving references from one node to the next: elem = elem.next. Each time we move this reference, Swift will increase the reference count of the next object and decrease the reference count of the previous object. Such reference-counting methods are expensive, but unavoidable as long as we use Swift’s classes.

  final class Node {
   var next: Node?
   var data: Int
   ...
  }
Copy the code

Suggestion: Use unmanaged references to avoid the overhead of reference counting

In performance-first code, you can choose to use unmanaged references. The Unmanaged

structure allows developers to turn off automatic reference counting (ARC) for special references.

    var Ref : Unmanaged<Node> = Unmanaged.passUnretained(Head)

    while let Next = Ref.takeUnretainedValue().next {
      ...
      Ref = Unmanaged.passUnretained(Next)
    }
Copy the code

agreement

Suggestion: Flag protocols that can only be implemented by classes as class protocols

Swift can restrict the protocol to classes only. One advantage of token protocols being implemented only by classes is that the compiler can optimize programs based on the fact that only classes implement protocols. For example, if the ARC memory management system knows that a class object is being processed, it can simply preserve (increase the object’s reference count) it. If the compiler does not know this fact, it has to assume that constructs can implement protocols as well, and it needs to be prepared to retain or release non-negligible constructs, which can be costly.

If you limit a protocol to be implemented only by a class, you need to mark the protocol implemented by the class as a class protocol for better performance.

  protocol Pingable : class { func ping() -> Int }
Copy the code

Developer.apple.com/library/ios…

footnotes

  • [1] The virtual method table, or ‘vtable, ‘is a type specific table referenced by an instance of the containing type method address. When dynamic distribution is performed, you first look up the table from the object and then look up the methods in the table.

  • [2] This is because the compiler does not know which function is called.

  • [3] For example, load a class domain directly or call a function directly.

  • [4] Explain what COW is.

  • [5] In some cases, the optimizer can remove retained references through direct inserts and ARC optimizations. This release ensures that copying does not occur.