Few ways of improving your iOS APP performance in Swift (2)

Gordon Feng
Towards Dev
Published in
5 min readJul 30, 2022

--

For the previous article, please see here.

In this Improving iOS application performance part 2, we will discuss the below topic.

  1. Using container types Efficiently
  2. Wrapping operations
  3. Generics
  4. The cost of large Swift values

Using container types Efficiently

Array and Dictionary are two generic containers in Swift, we can improve our performance with use these two wisely.

Use value types in Array

First, we should know the difference between NSArray and Array.

  1. Array: is a struct, as a value type.
  2. NSArray: is an Objective-C class, therefore it is a reference type and It is bridged(Toll-free bridge) to Array. And NSMutableArray is the mutable subclass of NSArray.

Due to the value type cannot being included inside an NSArray, the optimizer can remove most of the overhead in Array, which is necessary to handle the possibility of the array being backed by an NSArray.

Further, if we use reference type instead of value type, may cause retain cycle if we don't treat reference count properly. Using value types such as struct can avoid retain-cycle, and release traffic inside of Array.

Please note that there is a trade-off between using a large value type and a reference type. In certain cases, the overhead copying and moving value will outweigh the cost of removing the bridging and retain/release overhead.

Using contiguousArray with reference type when NSArray bridge is unnecessary

If you need an array and the array does not need to bridge to NSArray or envoke objc method, use contiguousArray instead of Array.

ContiguousArray

Use in-place mutation instead of reassignment

We all know Array is a Value type and it does copy on write(CoW)

let c: [Int] = [1,2,3]
var d = c // No copy will occur here.
d.append(4) // A copy *does* occur here.

Sometimes we will do something like the below cause the parameter of the function is a let:

var a: [Int] = []func addOne(with array: [Int]) {
var mutated = array
mutated.append(1)
a = mutated
}

In the above example, we input a into addOne method, cause array can not be modified during the let prefix, we reassign it into another var property by allocating another space.

But with input prefix, we can avoid this extra space to allocate, and modify this array in place.

func addOne(with array: inout [Int]) {
array.append(1)
}

Wrapping operations

Swift will check if the integer is overflow when performing normal arithmetic. These checks will not be appropriate in high-performance code if you already know that overflow will not occur.

var a: [Int] = []
var b: [Int] = []
var c: [Int] = []
for i in 0 ... n {
c[i] = a[i] + b[i]
}

Use wrapping integer arithmetic when you know that overflow won't occur.

Rather than performing +, us overflow operations like&+ instead if you insure the integer will not overflow.

var a: [Int] = []
var b: [Int] = []
var c: [Int] = []
// Precondition: for all a[i], b[i]: a[i] + b[i] either does not overflow,
// or the result of wrapping is desired.
for i in 0 ... n {
c[i] = a[i] &+ b[i]
}

It simply wraps around if it will overflow. Thus, Int.max &+ 1 is guaranteed to be Int.min.

Generics

Swift provides a very convenient generic type as an abstraction, by giving it a specific type to the generics type, the compiler will emit one block of concrete code that perform MySwiftFunc<T> for any T. The generated code takes a table of function pointers and a box containing T as additional parameters.

Any differences in behavior between MySwiftFunc<String> and MySwiftFunc<Int> are accounted for by passing a different table of function pointers and the size of the abstraction provided by the box.

class MySwiftFunc<T> { ... }/*
Will emit code, takes a table of function pointer and box that work with Int...
*/
MySwiftFunc<Int>
/*
Will emit code, takes a table of function pointer and box that work with String...
*/
MySwiftFunc<String>

When optimizations are enabled, Swift compiler will look at each invocation of such code and attempts to ascertain the concrete type used in the invocation.

So, when the generic definition is visible to the optimizer and the concrete type is known, Swift will process progress called Specialization, which compiler will emit a version of the generic function to the specific type, and enables the removal of the overhead associated with generics.

How does generic work?

class MyStack<T> {
func push(_ element: T) { ... }
func pop() -> T { ... }
}

func myAlgorithm<T>(_ a: [T], length: Int) { ... }

// The compiler can specialize code of MyStack<Int>
var stackOfInts: MyStack<Int>
// Use stack of ints.
for i in ... {
stackOfInts.push(...)
stackOfInts.pop(...)
}

var arrayOfInts: [Int]
// The compiler can emit a specialized version of 'myAlgorithm' targeted for [Int]' types.
myAlgorithm(arrayOfInts, arrayOfInts.length)

Put generic declarations in the same module where they used

The optimizer can only perform Specialization if the definition of the generic declaration is visible in the current Module. Specialization can only occur if the declaration is in the same file as the invocation of the generic, unless the WMO(Whole-Module-Optimization) flag is used.

NOTE: The standard library is a specific case. Definitions in the standard library are visible in all modules and will be available for Specialization.

The cost of large Swift values

In Swift, value type will have its unique copy of data during performing assignment, initialise, and argument passing, the program will create a new copy of the value. For some large value, these copies could be time-consuming and hurt the performance of the program.

protocol P {}
struct Node: P {
var left, right: P?
}

struct Tree {
var node: P?
init() { ... }
}

For the above example, we have the value Node stored in the Tree, when the Tree is copied, the whole nodes have to be copied. In this case, it is an expensive operation that required many calls of malloc/free and a significant reference counting overhead-O(n).

Use copy-on-write semantics for large value

To eliminate the cost of the large value via adopting copy-on-write behavior, the easiest way is to compose an exciting copy-on-write data structure like Array-O(1).

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

In this example, we eliminate the cost of copying the content of tree by wrapping it into an array. This simple change has a major impact and allows us to copy the content from O(n) to O (1).

The alternative to using the array is to implement a copy-on-write data structure as a value wrapper like the below:

If you think this help, please don’t forget to click the clap button. thanks:)

and we will see you in the next one.

--

--