Swift optimization summary

(WeChat public account) [ https://mp.weixin.qq.com/s/BuRr6EobhmgAu0XRLu9EgA ]

 

During the transformation process, Swift's efficiency, safety, convenience and some excellent features left a deep impression on the team. There are many features that developers don't think much about when writing ObjC. For example, Swift's static dispatch method, use of value types, static polymorphism, Errors+Throws, currying and function synthesis, rich higher-order functions, etc. Compared with OOP, Swift can also better support protocol-oriented programming. , generic programming, and more abstract functional programming solve many pain points faced by developers in the ObjC era.

Combining the similarities and differences between Swift and ObjC, we started from the advantages of Swift and re-examined and optimized the functional code of the project. The optimization points include but are not limited to the following aspects.

 

Replace dynamic dispatch of some methods with static dispatch

 

One of the reasons Swift runs faster than ObjC is the way it dispatches: static dispatch (value types) and function table dispatch (reference types). Using the static dispatch ARM architecture can directly use the bl instruction to jump to the corresponding function address, which has the highest calling efficiency and is conducive to the inline optimization of the compiler. The value type cannot inherit the parent class, and the type can be determined at compile time, which meets the conditions of static dispatch. For reference types, the settings of different compilers will also affect the dispatch method. For example, under WMO full module compilation, the system will automatically fill in keywords such as implicit final to modify classes that are not inherited by subclasses, so as to use static dispatch as much as possible.

In our project, an overall check was made for all classes that use Class. Unless necessary, inherit from NSObject and use NSObject subclasses sparingly. For scenarios where inheritance or polymorphism do not need to be considered, use keyword modifications such as final or private as much as possible.

Another thing to note is that ObjC also introduces static dispatch of methods. The latest LLVM integrated in Xcode12 already supports ObjC by specifying __attribute__((objc_direct)) on the method to change the original dynamic message dispatch to static dispatch.

 

Check all Classes and replace them with structures or enumerations if possible

 

Structures and enumerations in Swift are value types, and Class is a reference type. Whether to use value types or reference types in Swift is something developers need to think about and evaluate.

In the JD Logistics widgets we developed and the macOS applications developed based on SwiftUI, we currently use more structures and enumerations. First compare the differences between value types and reference types, value types (Struct Enum, etc.):

  • Created on the stack, the creation speed is fast

  • Small memory footprint. The overall memory occupied is the size of the internal attribute memory after alignment.

  • Memory recycling is fast, just use the stack frame to control the pushing and popping of the stack. There is no overhead of processing heap memory.

  • No reference counting is required (except for using reference types as attributes in structures)

  • Generally, it is statically dispatched, runs quickly, and is also convenient for compiler optimization, such as inlining, etc.

  • Deep copy during assignment. The system uses Copy-On-Write to avoid unnecessary copying and reduce copying overhead.

  • No implicit data sharing, independent immutability

  • Properties in the structure can be modified through mutating. In this way, while ensuring the independence of value types, it can also support the modification of some attributes.

  • Thread safety, generally speaking, there are no race conditions and deadlocks (note that the value must be copied in each sub-thread)

  • Inheritance is not supported to avoid the problem of OOP subclasses being too coupled to parent classes.

  • Abstraction can be achieved through protocols and generics. However, the structures that implement the protocol have different memory sizes, so they cannot be placed directly into the array. For storage consistency, the system will introduce the intermediate layer Existential Container when passing parameters and assigning values. Here, if there are more structure attributes, it will be a little more complicated, but Apple will also optimize (Indirect Storage With Copy-On-Write) with less overhead. Generally speaking, polymorphism of value types has a cost, and the system will try to optimize it. What developers should consider is: reduce dynamic polymorphism and use protocols directly as classes. They need to consider more static polymorphism and use it in conjunction with generic constraints.

 

 

 

Reference types (Class Function Closure, etc.):

 

  • The reference type is not as efficient as the value type in memory usage. It is created on the heap and needs a stack pointer to point to this area, which increases the overhead of heap memory allocation and recovery.

  • Assignment consumes little money and is generally a shallow copy of the pointer. But there is a reference counting cost

  • Multiple pointers can point to the same memory, with poor independence and easy misoperation.

  • Non-thread safe, atomicity must be considered, multi-threading requires thread lock cooperation

  • Reference counting is needed to control memory release. Improper use may lead to risks of wild pointers, memory leaks and circular references.

  • Inheritance is allowed, but the side effect of inheritance is the tight coupling between the subclass and the parent class. For example, the main purpose of the system's UIStackView is only for layout use, but it has to inherit all properties and methods of UIView.

 

It can be seen that Swift provides more powerful value types to try to solve typical pain points such as tight coupling between subclasses and parent classes of OOP in the ObjC era, implicit data sharing of objects, non-thread safety, and reference counting. If you look at the Swift standard library, you will find that it is mainly composed of value types, and collections of basic types such as Int, Double, Float, String, Array, Dictionary, Set, and Tuple are also structures. Of course, although the value type has many advantages, it does not mean that Class should be completely abandoned. It is still necessary to analyze according to the actual situation. The actual Swift development is more of a combination of methods, and it is unrealistic not to use OOP at all.

 

Optimize structure memory

 

Just like using C language structures, the size of the Swift structure is the size of the internal property memory alignment. Placing the properties in different orders in the structure will affect the final memory size. You can use the MemoryLayout provided by the system to check the memory size occupied by the corresponding structure.

We reviewed some details, such as there is no need to use Int in scenarios where Int32 is fully satisfied, do not use String or Int instead of Bool in scenarios where Bool should be used, try to put small-memory attributes at the back, etc.

struct GameBoard {
   
     var p1Score: Int32  var p2Score: Int32  var gameOver: Bool }struct GameBoard2 {
   
     var p1Score: Int32  var gameOver: Bool   var p2Score: Int32}//基于CPU寻址效率考虑,GameBoard2字节对齐后占用空间更多MemoryLayout<GameBoard>.self.size  //4 + 4 + 1 = 9(bytes)MemoryLayout<GameBoard2>.self.size //4 + 4 + 4 = 12(bytes)

 

Use static polymorphism instead of dynamic polymorphism

 

When we mentioned value types above, we mentioned static polymorphism. Static polymorphism refers to polymorphism in which the compiler can determine the type at compile time. In this way, the compiler can type downgrade and generate methods of a specific type at compile time.

Defining generics to comply with the constraints of a certain protocol can avoid using the protocol directly as a class to pass parameters, otherwise the compiler will report an error, which is equivalent to the interface supporting polymorphism, but it must be called with a specific type when calling, thus achieving Purpose of static polymorphism. For static polymorphism, the compiler will make full use of its static characteristics for optimization, and at the same time, when WMO Whole Module Optimization is set, it will try to control the possible code growth.

In short, developers should consider static polymorphism as much as possible. For example, when using protocols as parameters of functions, generics can be introduced. There is a classic discussion in WWDC:

protocol Drawable {

    func draw()}struct Line: Drawable {
   
       var x: Double = 0    func draw() {
   
       }}func drawACopy<T: Drawable>(local: T) {//指定T必须遵守Drawable    local.draw()}

let line = Line()drawACopy(local: line)//Success (传入具体的实现了Drawable的结构体,编译器可推断其类型)let line2: Drawable = Line()drawACopy(local: line2)//Error,编译器不允许直接使用Drawable协议作为入参

 

Protocol-oriented provides extended default implementations for protocols

 

For classes inheriting from parent classes and complying with protocols, Swift prefers the latter. In the form of OOP in ObjC, you can basically use Structs/Enums + Protocols + Protocol extensions + Generics to achieve logical abstraction in Swift.

We have minimized the use of OOP in the project, and only used value types for protocols and generics as much as possible. This way the compiler can do more static optimizations and reduce the tight coupling caused by OOP super classes.

At the same time, Protocol extension can provide a default implementation for protocol, which is also a very important optimization that is different from the ObjC protocol.

When using it, please note that you should use the specific type to call the method in the Protocol extension, rather than using the Protocol obtained through type inference. When calling using Protocol, if the method is not defined in Protocol, the default implementation in Protocol extension will be called, even if the corresponding method is implemented in the specific type. Because the compiler can only find the default implementation at this time.

 

Optimize error handling

 

Compared with ObjC, Swift handles Error and Throw more completely. The obvious benefits of this are that the API is more friendly, improves readability, and uses editor detection to reduce the probability of errors. In the ObjC era, people often do not consider the operation of throwing exceptions. This is something that programmers who are accustomed to ObjC coding need to pay attention to when encapsulating the underlying API. It is common to use an Enum that inherits the Error protocol.

 

enum CustomError: Error {
   
      case error1   case error2}

 

After an Error is generated, it can also be thrown for external processing. After the throw method is supported, the compiler will better detect whether the throw is processed. It should be noted that () throws -> Void and () -> Void are different Function Types.

​​​​​​​

//(Int)->Void可以赋值给(Int)throws->Voidlet a: (Int) throws -> Void = { n in}//反之类型不匹配 编译报错let b: (Int) -> Void = { n throws in}

 

rethrows: If a function input parameter is a function that supports throw, then rethrows can be used to identify that the function can also throw an Error. In this way, when using this function, the compiler will detect whether try-catch is needed.

This is what we need to consider when encapsulating basic functions. There are many friendly examples in the system, such as the definition of map function in the system:

​​​​​​​

public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]let a = [1, 2, 3]enum CustomError: Error {
   
     case error1  case error2}do {
   
     let _ = try a.map { n -> Int in    guard n >= 0 else {
   
         //如果map接受的closure内部有抛出throw,编译器会强制检测外部是否有try-catch      throw CustomError.error1    }    return n * n  }} catch CustomError.error1 {
   
   } catch {
   
   }

 

Use Guard to reduce if nesting

 

Guard can be used for criticality detection. Its advantage is that it can enhance readability and reduce excessive if nesting. When using Guard, generally else will be return, throw, continue, break, etc.

​​​​​​​

//if嵌套过多,难以阅读,增加后期维护成本if (xxx){
   
     if (xxx) {
   
       if (xxx) {
   
       }  }}//使用Guard,整体更清晰,便于后期维护let dict : Dictionary = ["key": "0"]guard let value1 = dict["key"], value == "0" else {
   
     return}guard let value2 = dict["key2"], value == "0" else {
   
     return}print("\(value1) \(value2)")

 

Use Defer

 

The closure modified by defer will be called when the current scope exits. It is mainly used to avoid repeatedly adding code that needs to be executed before returning and improve readability.

For example, in our macOS application, there are operations to read and write files. At this time, using defer can ensure that you will not forget to close the file.

​​​​​​​

func write() throws {
   
     //...  guard let file = FileHandle(forUpdatingAtPath: filepath) else {
   
       throw WriteError.notFound  }  defer {
   
       try? file.close()  }  //...}

 

Another common scenario is when releasing a lock, and non-escape closure callbacks, etc.

But don't overuse defer. When using it, pay attention to closure capture variables and scope issues.

For example, if you use defer in an if statement, the defer will be executed when the if is exited.

 

Replace all forced unpacking with optional binding

 

For optional values, force unpacking should be avoided as much as possible or even completely. In most cases, if you encounter a situation where you need to use !, it may mean that the original design is unreasonable. When including downCasting, since the type conversion itself may fail, avoid using as! and try to use as?. Of course, try! should also be avoided.

For optional values, always use optional binding detection to ensure that the optional variable has a real value before proceeding:

​​​​​​​

var optString: String?if let _ = optString {
   
   }

 

Consider lazy loading more

 

Change the properties in the project that do not need to be created to lazy loading. Swift's lazy loading is more readable and easier to implement than ObjC. Just use Lazy modification.

​​​​​​​

lazy var aLabel: UILabel = {
   
       let label = UILabel()    return label}()

 

Use functional programming to reduce state variable declaration and maintenance

 

Declaring too many state variables in a class is not conducive to later maintenance. Functions in Swift can be used as function parameters, return values, and variables, which can well support functional programming. Using functional expressions can effectively reduce global variables or state variables.

Imperative programming focuses more on the steps to solve a problem. Direct response to machine instruction sequences. There are variables (corresponding to storage units), assignment statements (corresponding to acquisition and storage instructions), expressions (corresponding to arithmetic calculations of instructions), and control statements (corresponding to jump instructions).

Functional programming pays more attention to the mapping relationship of data and the flow of data, that is, input and output. Functions are treated as variables, which can be used as arguments (input values) to other functions and can be returned from functions (output values). Describe the calculation as expression evaluation, the mapping of independent variables f(x)->y, given x, will be stably mapped to y. Try not to access variables outside the function scope within the function, and only rely on input parameters to reduce the declaration and maintenance of state variables. At the same time, use less mutable variables (objects) and more immutable variables (structures). This way there will be no interference from other side effects.

Use currying to transform a function that accepts multiple parameters into a function that accepts a single parameter, and cache some parameters inside the function . At the same time, function synthesis is used to increase readability. For example, when doing addition and multiplication calculations, we can encapsulate the addition and multiplication functions and call them one by one:

​​​​​​​

func add(_ a: Int, _ b: Int) -> Int { a + b }func multiple(_ a: Int, _ b: Int) -> Int { a * b }let n = 3multiple(add(n, 7), 6) //(n + 7) * 6 = 60

 

You can also use functional expressions:

​​​​​​​​​​​​​​

//柯里化add和multiple函数: 由两个入参改为一个并返回一个(Int)->Int类型函数func add(_ a: Int) -> (Int) -> Int { { $0 + a} } func multiple(_ a: Int) -> (Int) -> Int { { $0 * a} } //函数合成 自定义中置运算符 > 增加可读性infix operator > : AdditionPrecedencefunc >(_ f1: @escaping (Int)->Int,       _ f2: @escaping (Int)->Int) -> (Int) -> Int {
   
     {f2(f1($0))} }//生成新的函数 newFnlet n = 3let newFn = add(7) > multiple(6) // (Int)->Intprint( newFn(n) ) //(n + 7) * 6 = 60

 

It can be seen that from using multiple(add(n, 7), 6) to let newFn = add(7) > multiple(6), newFn(n), the overall situation is clearer, especially in more complex scenarios. Its advantages will be more obvious.

 

Summarize

 

Swift provides a wealth of simple syntactic sugar and powerful type inference, which make it easy to get started with Swift. However, from the perspective of performance considerations or designing a more perfect API, more practice is still needed. The order team is trying to use Swift and SwiftUI development as much as possible in scenarios such as iOS widgets, AppClips, JD workstation (macOS desktop application), etc., and the development efficiency and project stability have been good. At present, the infrastructure for Swift within the JD Group is gradually being improved. We believe and hope that more students in the group will participate in the development of Swift in the future.

Guess you like

Origin blog.csdn.net/zhuweideng/article/details/117350030