Last active
April 5, 2021 15:06
-
-
Save jaredkhan/5c77a7f79c6adcdf58c9a08c9d53a023 to your computer and use it in GitHub Desktop.
A few demonstrations of the copy-on-write behaviour of containers in the Swift Standard Library.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// A few demonstrations of the copy-on-write behaviour of containers in the Swift Standard Library. | |
/// https://jaredkhan.com/blog/swift-copy-on-write | |
/// Pause execution so that we can look at memory usage in Activity Monitor or similar. | |
func pause(expectedMemoryUsage: Int) { | |
print("Expecting memory usage of about \(expectedMemoryUsage)MB. Press [enter] to continue") | |
let _ = readLine() | |
} | |
/// Demonstrate the copy-on-write behaviour of a large array. | |
func largeArray() { | |
print("Demoing copy-on-write of a large array") | |
pause(expectedMemoryUsage: 0) | |
let x = Array(repeating: Int64(1), count: 100_000_000) | |
// 100M 8-byte integers should result in around 800MB of memory usage. | |
// You can check this in Activity Monitor or a similar program. | |
pause(expectedMemoryUsage: 800) | |
var y = x | |
// Both x and y have a reference to the same buffer. | |
pause(expectedMemoryUsage: 800) | |
y.append(2) | |
// Have made a mutating whilst both x and y are referencing the buffer. | |
// The buffer will be copied first, so expect higher memory usage. | |
pause(expectedMemoryUsage: 1600) | |
} | |
/// Demonstrate the copy-on-write behaviour of an array within an array. | |
func nestedArray() { | |
print("Demoing copy-on-write of a nested array") | |
pause(expectedMemoryUsage: 0) | |
let x = [ | |
Array(repeating: Int64(1), count: 100_000_000), | |
Array(repeating: Int64(1), count: 100_000_000), | |
] | |
// x's buffer doesn't itself hold a lot of data, | |
// rather its elements hold references to large buffers | |
pause(expectedMemoryUsage: 1600) | |
var y = x | |
y.append([]) | |
// Have made a mutation to y's buffer, but not to the large buffers of the elements | |
pause(expectedMemoryUsage: 1600) | |
y[0].append(2) | |
// Have mad e mutation to one of the large buffers, so that one buffer will be copied first | |
pause(expectedMemoryUsage: 2400) | |
} | |
func arrayInStruct() { | |
print("Demoing copy-on-write of an array within a struct") | |
pause(expectedMemoryUsage: 0) | |
struct ThingWithArray { | |
var name: String | |
var array: [Int64] | |
} | |
let x = ThingWithArray( | |
name: "Nice Thing", | |
array: Array(repeating: Int64(1), count: 100_000_000) | |
) | |
pause(expectedMemoryUsage: 800) | |
var y = x | |
// y is now a copy of x, but has a reference to the same array buffer | |
pause(expectedMemoryUsage: 800) | |
y.name = "Nicer Thing" | |
// mutating y does not touch the array buffer, so no large data is copied | |
pause(expectedMemoryUsage: 800) | |
y.array.append(1) | |
// now attempting to mutate the array buffer whilst reference count is >1, | |
// so a copy is taken | |
pause(expectedMemoryUsage: 1600) | |
} | |
func manyStructs() { | |
print("Demoing that structs don't do copy-on-write") | |
struct Thing { | |
// A struct with about 80 bytes | |
let a: Int64 = 0 | |
let b: Int64 = 0 | |
let c: Int64 = 0 | |
let d: Int64 = 0 | |
let e: Int64 = 0 | |
let f: Int64 = 0 | |
let g: Int64 = 0 | |
let h: Int64 = 0 | |
let i: Int64 = 0 | |
let j: Int64 = 0 | |
} | |
let thing = Thing() | |
let x = Array(repeating: thing, count: 10_000_000) | |
// Structs themselves do not have copy-on-write semantics, | |
// so expect this struct to have really been copied 1M times | |
pause(expectedMemoryUsage: 800) | |
} | |
func mutateWithOneReference() { | |
print("Demoing no copy when uniquely referenced") | |
pause(expectedMemoryUsage: 0) | |
var x = Array(repeating: Int64(1), count: 100_000_000) | |
pause(expectedMemoryUsage: 800) | |
({ | |
let y = x | |
pause(expectedMemoryUsage: 800) | |
}()) | |
// y has now gone out of scope so x's buffer's reference count = 1 | |
x.append(1) | |
pause(expectedMemoryUsage: 800) | |
} | |
largeArray() | |
nestedArray() | |
arrayInStruct() | |
manyStructs() | |
mutateWithOneReference() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment