Writing High-Performance Swift Code
Writing High-Performance Swift Code
When developing an iOS app, it’s critical that your app has good performance. Your users expect it, and it will hurt your reviews if your app appears unresponsive or slow.
Even during your interviews, Interviewer can ask you what you have done to write high performance swift code or how do you review the code for performance optimization?
In a nutshell, Our answer should be:
- Using an
NSDictionary
object to hold a single key-value pair is significantly more expensive than simply allocating a variable to hold the data. Creating thousands of such dictionaries wastes memory. - In performance critical code, one often will want to restrict this dynamic behavior using final keyword i.e. Reducing Dynamic Dispatch.
private
orfileprivate
keywords to a declaration restricts the visibility of the declaration to the file, allows the compiler to be able to ascertain all other potentially overriding declarations.- Using Container Types Efficiently, Use value types in Array: By using value types without reference types, one can avoid additional retain, release traffic inside.
- Use ContiguousArray with reference types when NSArray bridging is unnecessary.
- Use inplace mutation instead of object-reassignment
- Put generic declarations in the same module where they are used
- Use copy-on-write semantics for large values
- Mark protocols that are only satisfied by classes as class-protocols
We have seen the answer in nutshell, its time to dive into details. All the details mentioned in this article has been referred from apple swift docs only.
1. Dynamic Dispatch
Swift by default is a very dynamic language like Objective-C. Unlike Objective-C, Swift gives the programmer the ability to improve runtime performance when necessary by removing or reducing this dynamism.
In Swift, dynamic dispatch defaults to indirect invocation through a vtable [1]. If one attaches the dynamic
keyword to the declaration, Swift will emit calls via Objective-C message send instead. In both cases this is slower than a direct function call because it prevents many compiler optimizations [2] in addition to the overhead of performing the indirect call itself. In performance critical code, one often will want to restrict this dynamic behavior.
Thus in the following code snippet, a.aProperty
, a.doSomething()
and a.doSomethingElse()
will all be invoked via dynamic dispatch:
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 = ...
}
2. Use ‘final’ when you know the declaration does not need to be overridden
The final
keyword is a restriction on a declaration of a class, a method, or a property such that the declaration cannot be overridden. This implies that the compiler can emit direct function calls instead of indirect calls.
3. Use ‘private’ and ‘fileprivate’ when declaration does not need to be accessed outside of file
Applying the private
or fileprivate
keywords to a declaration restricts the visibility of the declaration to the file in which it is declared. This allows the compiler to be able to ascertain all other potentially overriding declarations. Thus the absence of any such declarations enables the compiler to infer the final
keyword automatically and remove indirect calls for methods and field accesses accordingly.
4. Use value types in Array
In Swift, types can be divided into two different categories: value types (structs, enums, tuples) and reference types (classes). A key distinction is that value types cannot be included inside an NSArray. Thus when using value types, the optimizer can remove most of the overhead in Array that is necessary to handle the possibility of the array being backed an NSArray.
Additionally, In contrast to reference types, value types only need reference counting if they contain, recursively, a reference type. By using value types without reference types, one can avoid additional retain, release traffic inside Array.
// Don't use a class here.
struct PhonebookEntry {
var name : String
var number : [Int]
}
var a : [PhonebookEntry]
Keep in mind that there is a trade-off between using large value types and using reference types. In certain cases, the overhead of copying and moving around large value types will outweigh the cost of removing the bridging and retain/release overhead.
5. Use ContiguousArray with reference types when NSArray bridging is unnecessary
If you need an array of reference types and the array does not need to be bridged to NSArray, use ContiguousArray instead of Array:
class C { ... }
var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]
6. Use inplace mutation instead of object-reassignment
All standard library containers in Swift are value types that use COW (copy-on-write) [4] to perform copies instead of explicit copies. In many cases this allows the compiler to elide unnecessary copies by retaining the container instead of performing a deep copy. This is done by only copying the underlying container if the reference count of the container is greater than 1 and the container is mutated. For instance in the following, no copying will occur when d
is assigned to c
, but when d
undergoes structural mutation by appending 2
, d
will be copied and then 2
will be appended to d
:
var c: [Int] = [ ... ]
var d = c // No copy will occur here.
d.append(2) // A copy *does* occur here.
Sometimes COW can introduce additional unexpected copies if the user is not careful. An example of this is attempting to perform mutation via object-reassignment in functions. In Swift, all parameters are passed in at +1, i.e. the parameters are retained before a callsite, and then are released at the end of the callee. This means that if one writes a function like the following:
func append_one(_ a: [Int]) -> [Int] {
a.append(1)
return a
}
var a = [1, 2, 3]
a = append_one(a)
a
may be copied [5] despite the version of a
without one appended to it has no uses after append_one
due to the assignment. This can be avoided through the usage of inout
parameters:
func append_one_in_place(a: inout [Int]) {
a.append(1)
}
var a = [1, 2, 3]
append_one_in_place(&a)
7. Put generic declarations in the same module where they are used
The optimizer can only perform specialization if the definition of the generic declaration is visible in the current Module. This can only occur if the declaration is in the same file as the invocation of the generic, unless the -whole-module-optimization
flag is used. NOTE The standard library is a special case. Definitions in the standard library are visible in all modules and available for specialization.
8. Use copy-on-write semantics for large values
To eliminate the cost of copying large values adopt copy-on-write behavior. The easiest way to implement copy-on-write is to compose existing copy-on-write data structures, such as Array. Swift arrays are values, but the content of the array is not copied around every time the array is passed as an argument because it features copy-on-write traits.
In our Tree example we eliminate the cost of copying the content of the tree by wrapping it in an array. This simple change has a major impact on the performance of our tree data structure, and the cost of passing the array as an argument drops from being O(n), depending on the size of the tree to O(1).
struct Tree : P {
var node : [P?]
init() {
node = [thing]
}
}
There are two obvious disadvantages of using Array for COW semantics. The first problem is that Array exposes methods like “append” and “count” that don’t make any sense in the context of a value wrapper. These methods can make the use of the reference wrapper awkward. It is possible to work around this problem by creating a wrapper struct that will hide the unused APIs and the optimizer will remove this overhead, but this wrapper will not solve the second problem. The Second problem is that Array has code for ensuring program safety and interaction with Objective-C. Swift checks if indexed accesses fall within the array bounds and when storing a value if the array storage needs to be extended. These runtime checks can slow things down.
An alternative to using Array is to implement a dedicated copy-on-write data structure to replace Array as the value wrapper. The example below shows how to construct such a data structure:
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 (!isKnownUniquelyReferenced(&ref)) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
}
The type Box
can replace the array in the code sample above.
9. Mark protocols that are only satisfied by classes as class-protocols
Swift can limit protocols adoption to classes only. One advantage of marking protocols as class-only is that the compiler can optimize the program based on the knowledge that only classes satisfy a protocol. For example, the ARC memory management system can easily retain (increase the reference count of an object) if it knows that it is dealing with a class. Without this knowledge the compiler has to assume that a struct may satisfy the protocol and it needs to be prepared to retain or release non-trivial structures, which can be expensive.
If it makes sense to limit the adoption of protocols to classes then mark protocols as class-only protocols to get better runtime performance.
protocol Pingable : AnyObject { func ping() -> Int }
Hope this article is useful for people looking to gain knowledge on high performance swift code or to crack iOS lead interviews, Please ❤️ to recommend this post to others 😊. Let me know your feedback. :)
References: