December 5, 2017·4 min read·engineeringswiftmemoryruntime

Swift 4.0.3 repairs temporary string retention on Darwin

A Swift 4.0.3 runtime change prevents temporary NSString values from accumulating in the caller's autorelease pool. The source and regression tests show why reference counting alone does not describe temporary memory lifetime.

A dictionary lookup can allocate temporary string storage even when the lookup finds nothing. On Darwin, Swift's String hashing can use Foundation to obtain a canonically decomposed string before hashing it. If that temporary object enters a long-lived autorelease pool, repeated lookups can retain memory that the program no longer needs. Swift 4.0.3 includes a runtime repair for that lifetime problem.

The December 5 release tag records the maintenance release as swift-4.0.3-RELEASE. Its comparison with 4.0.2 includes Arnold Schwaighofer's change to the String runtime stubs. The repair moves code into an Objective-C source file compiled with automatic reference counting, allowing the compiler to participate in the autoreleased-return-value handoff.

I like Swift's prompt reference-counted reclamation, but this repair shows why that description needs implementation detail. An object can have a short logical use and still remain in an autorelease pool. The relevant question is where the temporary is handed off and when the pool releases it.

The temporary belongs to a Foundation operation

The commit explanation describes String hashing on Darwin in terms of Foundation's hash function. For non-ASCII strings, the runtime calls decomposedStringWithCanonicalMapping. That operation can allocate an NSString and return it autoreleased. The previous implementation lived in a file compiled without ARC.

Under those conditions, the temporary could enter the currently active autorelease pool. A loop performing the operation repeatedly could therefore accumulate temporary objects until that pool drained. The source comments explicitly describe the risk of unbounded growth when the caller's pool has a sufficiently broad scope.

This is a retention problem with a specific cause. It does not require a cycle in the application's reference graph or a stored collection that grows on every iteration. The temporary result of an internal operation is enough. Looking only for references retained by application properties would miss the pool's role in extending the object's lifetime.

The distinction also explains why source-level value semantics are not the whole memory model. The program can use an ordinary Swift String as a dictionary key. Its runtime path can still invoke an Objective-C method that returns an autoreleased object. The implementation boundary determines how that temporary participates in memory management.

ARC enables the return-value handoff

The repair adds SwiftNativeNSXXXBaseARC.m to the runtime stubs and compiles it with -fobjc-arc on Darwin. Moving the relevant routines lets ARC insert objc_retainAutoreleasedReturnValue and the architecture-specific markers needed for the autoreleased-return-value optimization.

The comments explain the intended result. The temporary can be handed to the caller without first entering the caller's active autorelease pool. Once the hashing operation has finished with it, its lifetime no longer depends on waiting for that broader pool to drain. The change addresses the handoff at the runtime operation that creates the temporary.

The implementation has an explicit i386 exception. There, the Foundation method's implementation prevents the optimization, so the runtime uses a local autorelease pool. That confines the temporary lifetime without depending on the handoff optimization working on that architecture. Other platforms use the optimization described in the source.

The same new source file contains lowercase and uppercase string helpers. The change therefore treats related Foundation-returned temporary values together. It is not a change to String's public API, and callers do not need a new method name to benefit from the repaired runtime path.

The regression tests inspect memory and generated code

The patch adds a String memory test that repeatedly performs dictionary lookups and case conversions using non-ASCII strings. Its checks require the expected lookup behavior and examine peak resident memory. The test is restricted to an optimized standard library with Objective-C interoperability, matching the implementation path under examination.

The test's repetition count and memory threshold belong to that regression case. They are not a benchmark for an application or a claim about a universal reduction in memory use. What matters is that the workload is constructed to expose temporaries accumulating while the same operations repeat.

Another test inspects the runtime library's disassembly for the expected return-value optimization sequence. It checks calls and markers for several supported architectures. This complements the behavioral memory test by checking whether compilation produced the specific handoff on which the implementation relies.

I value that pairing because the fix depends partly on how the Objective-C compiler emits code. A source change that looks equivalent can alter the ownership handoff. Inspecting the generated sequence checks that assumption directly, while the repeated-operation test checks the memory behavior that motivated the change.

For applications using the affected Darwin paths, the maintenance release changes temporary storage lifetime beneath existing source. The code can continue using String hashing and case conversion normally. The tagged implementation handles the Foundation return values through ARC or a local pool, so those temporary objects do not have to accumulate in the caller's broader pool while repeated operations continue.