Why can a small slice keep a large array (or string) alive, and how do you stop it?
Predict first
When is each object freed?
final class Big { let id: Int; init(_ i: Int) { id = i }; deinit { print("freed \(id)") } }
func makeSlice() -> ArraySlice<Big> { let all = (0..<5).map { Big($0) }; return all[3..<5] }
func makeCopy() -> [Big] { let all = (0..<5).map { Big($0) }; return Array(all[3..<5]) }
var keep: ArraySlice<Big>? = makeSlice()
print("kept a slice of 2 elements")
keep = nil
print("--- copy instead")
var copy: [Big]? = makeCopy()
print("kept a copied array of 2 elements")
copy = nil
print("done")
- kept a slice…, freed 0–4 together, then the copy case frees 0, 1, 2 first and 3, 4 after “kept a copied array…”
- Only 0, 1 and 2 are freed in both cases
- Nothing is freed until the program exits
Mental model. A bookmark that holds the whole library: the page you want is short, but the library cannot close until you hand the bookmark back, unless you photocopy that page.
An ArraySlice or Substring is a view onto the original buffer: it stores a range and a reference to the parent storage. The elements outside the range stay alive as long as the slice does. In the check makeSlice builds five objects and returns only elements 3 and 4 as a slice: nothing is freed until the slice is dropped, then freed 0 … freed 4 appear together. makeCopy returns Array(all[3..<5]), which copies the two elements into a new buffer: elements 0 to 2 are freed right away and 3 and 4 live on. The same applies to a Substring of a megabyte string: keeping the last four characters pins the megabyte until you convert with String(sub).
This is the typical “abandoned memory” case (Q158): nothing leaks in the sense of Instruments’ Leaks, but memory stays reachable much longer than expected. The fix is simple: copy slices you intend to store (Array(slice), String(sub)), keep views for local processing only, and pay the small copy. Slices also keep the parent’s indices (a slice starting at 3 has startIndex == 3), which is a separate source of off-by-one bugs. Data slices behave the same way.
The slice shares the parent buffer; a copy releases the elements it does not keep.
Common traps
- Storing a
Substringin a model. It pins the whole source string. - Assuming
Array(slice)shares storage. It copies the elements (and retains class elements).
They might follow up with
- Does
prefixordropFirstcopy? They return slices; only the conversion copies. - Does the same hold for
Data? Yes, aDataslice shares and keeps the whole buffer.
Related questions
- 3.7.1 What is the difference between a leak, abandoned memory, fragmentation and a memory spike?
- 3.5.3 What is an Array in memory, and what is special about the empty array?
- 3.5.4 Why does a mutation sometimes copy the whole array, and how do you prove it?
← What is ManagedBuffer, and how does tail allocation put a header and… How do Array capacity and growth work, and when does an array give… →