Where do hidden retains and releases come from, and how do you reduce them?

3.6.1 · L5 · Interview

Predict first

In a Release profile, swift_retain and swift_release dominate an inner loop over an array of class instances. What is the best first change?

// A: wrap everything in unowned(unsafe)
// B: make the types final value types or index-based if ownership is not needed
// C: add autoreleasepool
  • A
  • B
  • C

Mental model. Every time you hand someone a book you both write your name on the checkout card. In a busy loop the clerk spends more time stamping than reading.

Every time a class reference is copied, the compiler adds a retain, and a release when the copy goes away. The common sources: assigning or storing a reference (into a property, array or dictionary); returning one (the caller receives a +1 it must release); capturing one in a closure; iterating for x in objects, which copies each element; and loading a reference property to call a method on it, where the compiler retains across the call in case the property changes underneath. The optimiser removes many pairs, but not all.

Each retain or release is an atomic operation on shared memory, and atomics get much more expensive when several cores touch the same object. In a profile this appears as swift_retain and swift_release near the top of Time Profiler. To reduce it: prefer value types and indices over webs of class references in hot loops, mark classes and methods final so the optimiser can see through them, pass by borrowing instead of copying, use ContiguousArray, and keep long-lived references in locals rather than reloading a property each iteration. Only reach for Unmanaged when a profile proves it.

Remove the reference traffic by design first (value types, indices, final). Unsafe references are a last resort, and an autorelease pool is not related to Swift’s own retains.

Common traps

  • Optimising retain counts without profiling. The compiler already removes most of them, and unsafe tricks cost correctness.
  • Assuming a let property is free to read. A class-typed property can still be retained around the call.

They might follow up with

  • Why are atomics costly? They force the cache line to move between cores. A single-threaded loop pays less than a contended one.
  • Which build do you profile? Release. The optimiser changes the retain count dramatically between Debug and Release.

Related questions

  • 3.1.1 How does ARC work, and how is it different from garbage collection?
  • 3.6.2 What do guaranteed and owned parameters mean, and what do borrowing and consuming add?
  • 3.6.3 What is Unmanaged, and what do takeRetainedValue and takeUnretainedValue mean?

What do guaranteed and owned parameters mean, and what do borrowing… →