Presenters:
- Ben Troller, Memory Tools Engineer
- Daniel Delwood, Runtime Tools Manager
Link: https://developer.apple.com/wwdc24/10173
Focus: Heap
- Memory allocated by
malloc()
, directly or indirectly - Memory with the most developer control. Where your apps reference types are stored.
- Almost always dirty, counts against limit
- Focus on measuring and reducing heap memory.
-
The heap is made up of multiple virtual memory regions
-
Each region of memory is broken up into individual heap allocations
-
Each region is made up of 16kb memory pages from the operating system, but each allocation can be bigger or smaller
-
Memory pages can be in one of three states:
- Clean: memory that hasn't been written to
- Dirty: Memory that has been written to recently by the application
- Swapped: when dirty pages haven't been used for a while, the system can swap them - compress them or write them to disk
-
Only dirty and swapped count toward an application's memory footprint
-
Developers don't call
malloc()
and friends directly, but the compiler and runtime use them -
malloc:
- Lifetime beyond the current scope, until
free()
- 16-byte minimum quanta and alignment
- Zeroes smaller blocks on free
- Lifetime beyond the current scope, until
-
Language runtimes use the heap to allocate long-lived memory. Swift, for example, expands this class initializer to a series of runtime functions, which end up calling malloc:
class ExampleClass {} let exampleInstance = ExampleClass()
-
Malloc has debugging features:
- MallocStackLogging: records call stacks and timestamps for each allocation
- In Xcode, can enable with a checkbox in the scheme diagnostics tab
- MallocStackLogging: records call stacks and timestamps for each allocation
-
Tracking memory usage:
- Xcode memory report: shows an application's footprint over time
- Memory Graph Debugger: can capture a snapshot of all allocations and the references between them
- With MallockStackLogging, this includes backtraces for each allocation
- Command-line tools for memory analysis. Leaks, heap, vmmap, malloc_history can analyze macOS and Simulator processes directly, or investigate issues using already-captured memory graphs.
- Instruments: profiling memory use over time
- Allocations: records the history of all allocation and free events over time
- Leaks: takes periodic snapshots of your app's memory to detect memory leaks
- Use the Product -> Profile menu item to perform a release build of the app and open Instruments with it selected as the target
- When Instruments opens, choose a template for profiling. Allocations.
- Press record to run the app and record data
- Save the trace, and you can share it with someone (File -> Save)
-
Memory spikes are a type of transient memory growth. Transient memory growth is bad because:
- Sudden memory pressure
- Out-of-memory crashes
- Fragmentation
- Terminating background tasks
- Termination of your app
-
Two ways to track transient growth:
- Look at a specific spike to find the allocations Created & Still Living from the low point to the high point
- In aggregate, select a large range and find all allocations that were Created & Destroyed in that range
-
Autorelease pools can be a source of transient memory growth
-
Implicit autorelease pools can have long lifetimes
-
Fix is often to define an explicit autorelease pool to reduce the lifetime of the allocated objects
-
Memory that doesn't get deallocated
-
Persistent growth often resembles a stairstep pattern, with memory increasing over time
-
The growth is made up of multiple allocations
-
Use the Mark Generation feature in the Allocations instrument to break down the growth by timespan
-
Types of references
- Strong: definitely a pointer, explicit ownership
- Weak/Unowned: definitely a pointer, explcit non-ownership
- Unmanaged: probably a pointer, manually managed
- Conservative: might be a pointer, tools must assume it is
- Memory leaks: in Instruments, allocations with a yellow triangle next to them
- Reachability:
- Finding paths from roots to heap blocks
- Three types of memory on your heap:
- Useful memory: reachable allocations that will be used again
- Abandoned memory: reachable allocations that will not ever be used again
- Leaked memory: unreachable memory that can't ever be used again - typically when the last pointer is lost, either to a manually managed allocation or a reference cycle of objects
- For most leaks, the goal is to find and fix one reference in a cycle
- Removing an accidental reference
- Changing ownership qualifier from strong to weak or unowned
- Instruments: use the "Show only leaked allocations" button in the filter bar
- Filter to only project types by clicking the other filter button
- Common Swift runtime types
Closure context
- Paired 1:1 with active closure
- Reference qualifiers, no capture names
- Closures capture references strongly by default, making it possible to create reference cycles
- You can break these cycles using weak or unowned captures instead
let swallow = Swallow() swallow.completion = { print("\(swallow) finished carrying a coconut") }
Why isn't a specific allocation shown as leaked?
- Leak scanning is conservative, meaning it allows uncertain references
How can the number of leaks go down over time?
- Conservative references aren't deterministic
Why is my noreturn
function leaking?
- Compiler calls to
noreturn
or-> Never
don't cleanup local references - Can explicitly store this into a global that the reference tools can see
- Common tools to use in Swift to avoid creating strong reference cycles - but when to use each?
If not seeing weak or unowned references reported in the memory graph, check the project Reflection Metadata Level in Build settings
- Default to 'All' level if possible
Weak references are:
- Always optional types
- Become nil after their destinations are deinitialized
- You can always use a weak reference, regardless of source and destination lifetimes
- Does come with overhead - Swift allocates a Swift weak reference storage for the destination object, which sits between the object and all of its incoming weak references
Unowned references:
- Directly hold their destinations
- They don't use any extra memory, and take less time to access than weak references
- Can be non-optional
- Can be constant
- It's not always valid to use an unowned reference
- If the object holding the unowned reference goes away before the reference, the object is deinitialized but not deallocated
- The unowned reference must point to something, so the runtime keeps the object around
- If you try to access the deinitialized object, you get a deterministic crash
- In this way, unowned references are a lot like force-unwrapping weak references
- As long as the unowned reference exists, the destination can't be deallocated and it wastes memory
- If you don't know how long the destination will live, the small overhead of a weak reference is worthwhile
Implicit used of self by a method causing a reference cycle:
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = defaultAction // Implicitly uses `self`
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
}
Break the reference cycle using a weak reference:
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = { [weak self] data in
return self?.defaultAction(data)
}
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
}
Break the reference cycle using an unowned reference:
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = { [unowned self] data in
return self.defaultAction(data)
}
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
}
- Don't circumvent Automatic Reference Counting (ARC)
- Ensure
-whole-module-optimization
in Swift - Fewer
any
boxes, retainable references in Swift structsstruct Nontrivial { var number: Int64 var simple: CGPoint? var complex: String // Copy-on-write, requires non-trivial struct init/copy/destroy }
Check out:
- Explore Swift performance
- Consume noncopyable types in Swift