Premature optimisation is the root of all evil. But, there are moments where we need to optimise our code. Let’s see how to improve the performance of value type in Swift.
Introduction
A variable in Swift can be either reference or value type.
The most basic distinguishing feature of a value type is that copying — the effect of assignment, initialization, and argument passing — creates an independent instance with its own unique copy of its data
In this article, I explain an optimisation of this copying: the copy-on-write.
What Is Copy-On-Write?
Every time we assign a value type to another one, we have a copy of the original object:
let myString = "Hello"
var myString2 = myString // myString is copied to myString2
myString2.append(" World!")
print("\(myString) - \(myString2)") // prints "Hello - Hello World!"
If we copy just a plain String we may not have problems with the performance.
We may start having some troubles when we have Arrays with thousands of elements, and we copy them around our app. For this reason, the Array has a different way to copy, which is called copy-on-write.
When we assign an Array to another one, we don’t have a copy. The two Arrays share the same instance. In this way, we don’t have two different copies of a big Array and we can use the same instance, with a better performance. Then, just when one of the two Arrays change we have a copy.
Let’s see an example:
var array1 = [1, 2, 3, 4]
address(of: array1) // 0x60000006e420
var array2 = array1
address(of: array2) // 0x60000006e420
array1.append(2)
address(of: array1) // 0x6080000a88a0
address(of: array2) // 0x60000006e420
We can notice, in this example, that the two Arrays share the same address until one of them changes. In this way, we can assign array1
to other variables several times without copying every time the whole array, but just sharing the same instance until one of them changes. This is very useful for the performance of our App.
For your info, this is the function used to dump the address:
func address(of object: UnsafeRawPointer) -> String {
let addr = Int(bitPattern: object)
return String(format: "%p", addr)
}
Unfortunately, not all the value types have this behaviour. This means that, if we have a struct with a lot of information, we may need a copy-on-write to improve the performance of our App and to avoid useless copies. For this reason, we have to create this behaviour manually.
Manual Copy-On-Write
Let’s consider a struct User
in which we want to use copy-on-write:
struct User {
var identifier = 1
}
We must start creating a class, with a generic property T
, which wraps our value type:
final class Ref<T> {
var value: T
init(value: T) {
self.value = value
}
}
We use class
—which is a reference type—because when we assign a reference type to another one the two variables will share the same instance, instead of copying it like the value type.
Then, we can create a struct
to wrap Ref
:
struct Box<T> {
private var ref: Ref<T>
init(value: T) {
ref = Ref(value: value)
}
var value: T {
get { return ref.value }
set {
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
ref.value = newValue
}
}
}
Since struct
is a value type, when we assign it to another variable, its value is copied, whereas the instance of the property ref
remains shared by the two copies since it’s a reference type.
Then, the first time we change value
of one the two Box
variables, we create a new instance of ref
thanks to:
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
In this way the two Box
variable don’t share the same ref
instance anymore.
isKnownUniquelyReferenced returns a boolean indicating whether the given object is known to have a single strong reference.
Here the whole code:
final class Ref<T> {
var value: T
init(value: T) {
self.value = value
}
}
struct Box<T> {
private var ref: Ref<T>
init(value: T) {
ref = Ref(value: value)
}
var value: T {
get { return ref.value }
set {
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
ref.value = newValue
}
}
}
We can use this wrapping like this:
let user = User()
let box = Box(value: user)
var box2 = box // box2 shares instance of box.ref
box2.value.identifier = 2 // Creates new object for box2.ref
Conclusions
An alternative to this approach is using an Array—instead of Box
—to wrap the value type to copy on write. Unfortunately, the approach with the Array has some disadvantages. You can find more details in the Apple’s optimisation tips.
11/03/2018 at 14:06
More investigation, seems that at least in Swift version 4.0.3, structs have copy on write enabled by default. See this code:
import UIKit
struct Address {
var country: String
}
struct AddressBits {
let underlyingPtr: UnsafePointer<Address>
let padding1: Int
let padding2: Int
}
//*******
var MyAddress1: Address = Address(country: "Spain")
var MyAddress2 = MyAddress1
print("MyAddress1 : \(unsafeBitCast(MyAddress1, to: AddressBits.self).underlyingPtr)")
print("MyAddress2 : \(unsafeBitCast(MyAddress2, to: AddressBits.self).underlyingPtr)")
print("\n---------------------------------------------\n")
MyAddress2.country = "USA" // Creates new object
print("MyAddress1 : \(unsafeBitCast(MyAddress1, to: AddressBits.self).underlyingPtr)")
print("MyAddress2 : \(unsafeBitCast(MyAddress2, to: AddressBits.self).underlyingPtr)")
Sample result:
“`MyAddress1 : 0x0000000112a32000
MyAddress2 : 0x0000000112a32000
MyAddress1 : 0x0000000112a32000
MyAddress2 : 0x0000000112a320b0“`
11/03/2018 at 14:24
That’s very interesting. I’ll have a look at it asap. Thank you for pointing it you !
10/03/2018 at 20:16
What if we fo this for adding thread safety?
@discardableResult
public func synchronized<T>(_ object: Any, closure: () throws -> T) rethrows -> T {
objc_sync_enter(object)
defer {
objc_sync_exit(object)
}
return try closure()
}
and then struct Box can be changed to this:
“`struct Box {
private var ref: Ref
init(value: T) {
ref = Ref(value: value)
}
}“`
Probably is overcomplicating things, but it should be thread safe.
10/03/2018 at 11:39
Hi Marco,
Interesting article thanks.
My doubt is when we should start to consider to use manual Copy on Write for a structure type? Apple doc says “To eliminate the cost of copying large values…” So what can be a large value? How that cost can be measured? Do you have any reference to benchmarks / memory usage using COW?
And lastly, how you solve the non thread safe nature for the copy on write?
Best
11/03/2018 at 14:24
Hi Pablo, I think an example of a large value can be a struct with which contains an array with a lot of elements. Of course, it’s not something you use all days. I think CoW depends in situations and it might not make sense is some of them.