srk1nn.blog

about iOS development

Published on 4 Mar 2024

Advanced Memory Management | Benchmarks in Swift

ARC automates the process of memory management. But sometimes, the usual tools are not enough. So today, we will talk about advanced techniques for memory management and compare them.

ARC techniques

Strong references

A strong reference is just a regular object usage. Creating a variable, or a constant, or saving a reference to an object in another object’s property — they all create a strong reference.

This type of reference — is the fastest. When you access a variable by a strong reference, Swift can miss any runtime checks. Since the compiler guarantees that the object always be alive.

Weak references

Weak reference helps to avoid retain cycles. But from a performance perspective — it’s the slowest reference type. Firstly, it returns nil, if an object doesn’t exist. So at runtime we have to perform additional checks to handle this. Secondly, weak reference points to a side-table. So even if an object exists, we must make 2 dereferences (to a side-table → to an object).

Unowned references

One of the popular question in iOS interviews is «What’s the difference between weak and unowned ?». The right answer is performance. It makes access to an object faster than weak reference, since unowned already points to an object. But Swift still performs any additional checks to throw runtime error, if an object is already deinited.

More performance, less safety

In ARC we have another reference type called unowned(unsafe). As well for unowned, ARC doesn’t increment a reference counter. But for this type, Swift doesn’t perform any checks and doesn’t throw runtime errors. It simply returns something at a given address. It can be an object, a garbage value, or nothing at all. So it could lead to a dangling pointer and undefined behavior.

unowned(unsafe) also came from Objective-C where it is called unsafe_unretained. You can see it in Core Data.

Manual reference counting

Sometimes you may better know when your object should live and where to destroy it. And to get more performance you can use manual reference counting. Thanks to Unmanaged.

In fact, Unmanaged uses unowned(unsafe) internally. And provides several methods for manual reference management, such as retain/release and so on. So you can eliminate ARC overhead.

Benchmarks

Let’s look at a brief benchmark to confirm our considerations.

final class Counter {
    var count = 0
}

func measure() -> UInt64 {
    var counters = ContiguousArray<Counter>()
    var total = 0

    for _ in (0..<10_000) {
        counters.append(Counter())
    }

    let start = DispatchTime.now().uptimeNanoseconds

    for index in counters.indices {
        let counter = counters[index] // [1] change reference type here
        for i in (0..<1_000) {
            counter.count += i
            total += counter.count
        }
    }

    let end = DispatchTime.now().uptimeNanoseconds

    return end - start
}

print(measure())

In this function, we iterate over classes and make computations accessing class properties. At [1] I specified reference type, like strong / weak / unowned / unowned(unsafe). I also tested Unmanaged. I used ContiguousArray<Unmanaged<Counter>>() for Unmanaged and added counters through .passRetained(Counter()). And I got the count property through counter.takeUnretainedValue().count. I also released all objects before return.

 

I have ran benchmarks on a macOS app template (release builds) 5 times each. And here are my average results.

As you can see, strong is the fastest one. Unmanaged and unowned(unsafe) performance are the same, since Unmanaged uses unowned(unsafe) internally. And they are slower than strong, because the compiler inserts additional retain when we access variables managed by this reference. Unowned is a little bit slower, due to runtime checks. Finally, weak reference is far behind…

I hope this article has been helpful!