Copy on Write in Swift explained with examples
Theory
According to Wikipedia - Copy on Write "is a resource-management technique used in computer programming to efficiently implement a "duplicate" or "copy" operation on modifiable resources."
As confusing as it may seem, the mechanism behind is very simple - we want to make a copy of the resource (class, struct, etc) only when it is mutated. If we just use resources as read-only, it stays in one place.
Swift Copy on Write
Swift has two basic data types - reference type (class, closure) and value type (struct, enum, touple).
Reference type points to the same place in memory. Every time you assign a reference type to a new value it implicitly points to the shared object.
Value type is copied to the new place every time you assign, initialize, and pass it as an argument.
This is true for almost all of the swift structs. But some of them are special. For example Arrays and other Collections. They implement copy on write mechanism. This means the array will be copied only when we mutate its content (not always - I'll show you an exception!).
Coding example
We will create a new command line tool for testing our assumptions. We will need something to print memory addresses, dummy structures and classes, and testing code.
Helpers
Our helpers will print addresses to the console:
// Prints address of structure
func address(o: UnsafeRawPointer) {
print(NSString(format: "%p", Int(bitPattern: o)))
}
// Prints address of class
func addressHeap<T: AnyObject>(o: T) {
print(NSString(format: "%p", unsafeBitCast(o, to: Int.self)))
}
Dummy types
We will need one dummy class and one dummy struct for demonstration:
struct Foo {
var foo = 1
}
class Bar {
var bar = 1
}
Testing
We are ready to test swift memory management! I'll start with a basic test, is class really reference type and struct value type?
All right! We can see by addresses, that struct Foo
is copied to a new place in memory, but class Bar
is referenced and pointed to shared space! Also, classes and structs are far apart in memory, since they are stored differently (stack/heap). For our custom struct, there is no Copy on Write mechanism. We have to check what will happen for the swift array!
First I'll test an array of structs:
We can see, before mutation, two arrays point to the same place in memory - 0x600001700720
. After mutation, one of them is copied to the new place 0x600001700760
. Copy on write works!
But will it always work for an array? Maybe we can "break" it a bit. Let's test it with an array of classes!
Now that doesn't seems right! We mutate barArray1
content but it is not copied! Why?! The answer is simple, since the class is a reference type, we don't mutate its reference, we change its property inside the shared instance. There is no change of address barArray1[0]
so there is no copying of the array. This will always happen when we store the reference type inside the value type.
Can I make my struct to be CoW?
Of course! If you feel it is important (the struct is heavy to copy and most of the time it is used without mutating, why not!), Apple gives us tips on how to do that on GitHub:
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
}
}
}
We can also leverage new swift features to elevate the solution to be more readable by using a simple property wrapper:
@propertyWrapper
public struct CopiedOnWrite<T: Any> {
public let wrapped: Box<T>
public var wrappedValue: T {
return wrapped.value
}
public init(wrappedValue: T) {
wrapped = Box(wrappedValue)
}
}
With a neat trick, we don't have to think when using our Copy on Write mechanism - the property wrapper will allow us to use any value as it is plain var.