Swift. Copy-On-Write

Maxim Krylov
3 min readJul 12, 2020

Here is a quick introduction, along with the full articles list

… is all about improving the performance of value types

Copying on write is only about structures that are stored on the heap. It makes real copies of instances only when that copies are being changed

String and all collection types — Array, Dictionary, and Set support this technique (they’re implicitly stored on the heap). But, copy-on-write doesn’t work for all structures by default. When simply copying a structure, it is copied even if its copies are not supposed to be changed. Though the compiler is free to optimize any structure access and effectively give you copy-on-write semantics, this is not guaranteed

Assignment, initialization, and arguments passing create a new copy of value type data or one more reference to reference type data

Array

Array filtering makes a new array

var array = [1, 2, 3, 4, 5]
// 0x6000038a0200
var filteredArray = array.filter { $0 > 3 }
// 0x6000015b3380

Casting to a new type makes a new array

var array = [1, 2, 3, 4, 5]
// 0x6000034393d0
var array2 = array as [Any]
// 0x600002234ce0

Creating a new array from a given one makes one more new array

var array = [1, 2, 3, 4, 5]
// 0x600001127860
var array2 = Array(arrayLiteral: array)
// 0x600003c3f6b0

An array supports copy-on-write

var array = [1, 2, 3, 4, 5]
// 0x600002e30160
var array2 = array
// 0x600002e30160
array2.append(6)
// 0x600002636320

ArraySlice

ArraySlice supports copy-on-write

final class Element {
let id: Int

init(id: Int) {
self.id = id
}

deinit {
print("deinit of '\(id)' is called")
}
}
var array: Array<Element>? = [Element(id: 1), Element(id: 2)]
// array is [Element(1), Element(2)]
var slice: ArraySlice<Element>? = array!.dropFirst()
// slice is [Element(2)]
slice![slice!.endIndex - 1] = Element(id: 3)
// slice is [Element(3)]
// array is [Element(1), Element(2)]
array = nil
// deinit of '1' is called
// deinit of '2' is called
var array2: Array<Element>? = [Element(id: 10), Element(id: 20)]
// array2 is [Element(10), Element(20)]
var slice2: ArraySlice<Element>? = array2!.dropFirst()
// slice2 is [Element(20)]
slice2!.append(Element(id: 30))
// slice2 is [Element(20), Element(30)]
// array2 is [Element(10), Element(20)]
array2 = nil
// deinit of '10' is called
var array3: Array<Element>? = [Element(id: 100), Element(id: 200)]
// array3 is [Element(100), Element(200)]
var slice3: ArraySlice<Element>? = array3!.dropFirst()
// slice3 is [Element(200)]
array3![array3!.count - 1] = Element(id: 300)
// array3 is [Element(100), Element(300)]
// slice3 is [Element(200)]
array3 = nil
// deinit of '300' is called
var array4: Array<Element>? = [Element(id: 1000), Element(id: 2000)]
// array4 is [Element(1000), Element(2000)]
var slice4: ArraySlice<Element>? = array4!.dropFirst()
// slice4 is [Element(2000)]
array4!.append(Element(id: 3000))
// array4 is [Element(1000), Element(2000), Element(3000)]
// slice4 is [Element(2000)]
array4 = nil
// deinit of '3000' is called

When a slice is changed, it loses a strong link to the original array’s entire storage. But if the original array is changed, its slices still have a strong link to the old version of the array

Set

A set supports copy-on-write

var set: Set = [1, 2]
// set is [1, 2]
var set2 = set
// set2 is [1, 2]
set2.insert(3)
// set2 is [1, 2, 3], set is [1, 2]

Dictionary

A dictionary supports copy-on-write

var dictionary = [1: "One", 2: "Two", 3: "Three"]
var dictionary2 = dictionary
dictionary2[1] = "1"
// dictionary = [3: "Three", 1: "One", 2: "Two"]
// dictionary2 = [3" "Three", 1: "1", 2: "Two"]

Manual Copy-On-Write

struct User {
var id: Int
var age: Int
}
final class ReferenceTo<T> {
var value: T
init(value: T) {
self.value = value
}
}
struct Box<T> {
private var reference: ReferenceTo<T>

init(value: T) {
reference = ReferenceTo(value: value)
}
var value: T {
get { return reference.value }
set {
// returns true if there is only one strong reference
if isKnownUniquelyReferenced(&reference) {
reference.value = newValue
}
reference = ReferenceTo(value: newValue)
}
}
}
var boxedUser = Box<User>(value: User(id: 1, age: 18))
// boxedUser2.value refers to boxedUser.value
var boxedUser2 = boxedUser
boxedUser2.value.id = 2 // copying on write
// boxedUser2.value.id is 2, boxedUser.value.id is 1

isKnownUniquelyReferenced

The function considers only strong references:

  • true == strong
  • false == strong, strong
  • false == weak, unowned
  • true == strong, weak, unowned

Resources

--

--