Swift concurrency
An actor protects state. It does not own every retain and release.
Actor isolation is a concurrency guarantee. ARC is a lifetime mechanism. Treating one as a complete proof about the other is where the mental model starts to drift.
6 min read
Audio edition is being recorded
Swift actors make an important promise.
They isolate mutable state.
That promise is strong enough for Swift 6 to reject programs that cross concurrency boundaries unsafely, and it gives us a much better language for reasoning about data races.
But there is a tempting shortcut hiding inside that model:
If an object is actor-isolated, its references must be isolated too.
From there it is easy to jump again:
If those references are isolated, ARC should not need atomic reference counting.
That sounds reasonable.
It is also more than actor isolation actually promises.
Start with the guarantee actors do make
An actor protects its actor-isolated state from concurrent access that violates Swift's isolation rules.
An actor also conforms to Sendable.
Those are language-level concurrency properties.
They help answer questions like:
- Can this mutable state be accessed from here?
- Does this value cross an isolation boundary legally?
- Does this code need
await? - Is the compiler able to prove that a particular access is safe?
Those are valuable guarantees.
But notice what is missing.
Actor isolation does not say:
Every reference-count operation associated with every object reachable from this actor happens only while executing on this actor.
That would be a much broader lifetime guarantee.
A reference can participate in more than one mechanism
Consider an actor that stores a class instance:
final class Payload {
let value: Int
init(value: Int) {
self.value = value
}
}
actor Store {
private var payload = Payload(value: 42)
func currentValue() -> Int {
payload.value
}
}The actor isolates access to payload as actor state.
That tells you something important about how the property can be accessed.
But ARC is not merely a record of source-level property accesses.
Reference lifetime can be affected by generated temporaries, function arguments, return values, captures, ownership conventions, optimizer decisions and other lowering details.
So there are two different questions:
Who may access this state?and:
How is this object's lifetime represented and updated in generated code?The first is where actor isolation operates.
The second belongs to ownership, ARC and compiler lowering.
This is exactly what the ARC experiment tested
I built a small Swift 6 lab around this distinction.
The negative control was a deliberately illegal case: non-Sendable mutable reference state crossing a concurrency boundary.
Swift 6 rejected it.
Then I tested valid cases using actor isolation, Sendable, and @unchecked Sendable.
Those cases compiled.
If concurrency isolation also acted as a direct ARC atomicity selector, this would be a reasonable place to expect the generated reference-counting path to change.
In the Apple Swift 6.3.3 cases I tested, it did not.
Ordinary builds still emitted:
swift_retain
swift_releaseThe actor-isolated and Sendable cases did not switch those observed calls to:
swift_nonatomic_retain
swift_nonatomic_releaseThen I changed a different assumption.
I compiled with:
-Xfrontend -assume-single-threadedNow the observed calls changed to the non-atomic variants.
That is a much cleaner signal than trying to infer ARC behaviour from the presence of actor or Sendable.
The A/B
For the controlled retain/release case in the lab:
| Build | Retain | Release |
|---|---|---|
normal -Onone | swift_retain | swift_release |
single-threaded -Onone | swift_nonatomic_retain | swift_nonatomic_release |
normal -O | swift_retain | swift_release |
single-threaded -O | swift_nonatomic_retain | swift_nonatomic_release |
The result is deliberately narrow.
It does not prove that Swift ARC is always atomic.
It does not prove that actor isolation can never participate in an ARC optimization.
It does not prove that Sendable can never influence optimization.
It shows that in these tested Apple Swift 6.3.3 cases, changing the concurrency validity of the program did not change the observed ARC runtime entry-point selection, while the explicit single-threaded compiler assumption did.
That is compiler/runtime implementation evidence for the tested toolchain, not a Swift language guarantee.
Why `Sendable` does not close the gap
Sendable is another place where the same mental shortcut appears.
A type conforming to Sendable is saying something about crossing concurrency boundaries safely under Swift's concurrency model.
For unchecked conformance:
final class Box: @unchecked Sendable {
// You are taking responsibility for the safety contract.
}the compiler is not proving your synchronization strategy for you.
You are asserting that the type can be used safely across those boundaries.
That still does not mean:
Sendable
↓
non-atomic refcountSendable is not an ARC annotation.
The Swift documentation describes @unchecked Sendable in exactly that spirit: you assume responsibility for correctness when the compiler cannot enforce the conformance itself.
Isolation is about access, not exclusive lifetime ownership
The easiest way to remember the distinction is:
Actor isolation controls access to isolated state. It does not mean the actor is the only place where all lifetime bookkeeping for every related reference can occur.
That difference becomes more visible when values move across APIs.
An actor method can return a Sendable value.
A reference may be captured by generated code.
A function call may introduce ownership traffic that is not interesting at the source level.
Optimization may remove, combine or reposition retain/release operations.
None of that means actor isolation is broken.
It means source-level isolation and runtime lifetime accounting are different layers of the implementation.
Why this matters for performance reasoning
This is where developers can accidentally turn a correctness feature into an imagined performance guarantee.
For example:
I moved this behind an actor, so ARC can be cheaper now.
That conclusion does not follow from the actor model.
The actor may absolutely improve the correctness and architecture of the code.
It may also change performance in many ways, including scheduling and serialization costs.
But if your performance claim is specifically about reference-count atomicity, you need evidence from generated code or runtime behaviour.
Do not infer it from actor.
Do not infer it from Sendable.
Measure the layer you are making the claim about.
Why SIL alone is not enough
This also explains why looking at one intermediate representation can be misleading.
SIL exposes ownership operations, but the question in the ARC experiment was narrower:
Which runtime ARC entry points survive into the generated program?
For that, LLVM IR and assembly are more directly useful.
Optimization can eliminate or combine ARC traffic, so even the presence of an ownership operation in one stage does not guarantee a corresponding runtime call later.
This is why the lab keeps SIL, LLVM IR and assembly evidence separate.
The more specific your claim, the closer your evidence should be to the layer you are claiming something about.
A better senior-level mental model
When reasoning about Swift concurrency and ownership, keep three layers separate:
1. Isolation
Who is allowed to access this state, and from which concurrency domain?
Actors and global actors live here.
2. Transfer
Can this value cross a concurrency boundary safely?
Sendable lives here.
3. Lifetime accounting
How does the generated program keep the referenced object alive and eventually destroy it?
ARC and compiler lowering live here.
These layers interact.
They are not interchangeable.
That is why a program can become valid under Swift 6's concurrency checker while the ARC entry points observed in generated code remain unchanged.
The practical rule
If Swift 6 rejects the program, fix the concurrency problem.
If you want to know what ARC is doing, inspect ARC.
Do not use one as a proxy for the other.
That sounds obvious when written down.
It is much less obvious when an actor has just given the compiler a strong new proof and you are staring at a pair of swift_retain / swift_release calls that refuse to disappear.
The compiler is not contradicting itself.
You were asking two different questions.
Reproduce the ARC experiment
The full lab is public:
https://github.com/salari-dev/swift6-arc-atomicity-lab
It includes:
- the negative concurrency control;
- actor-isolated,
Sendable, and@unchecked Sendablecases; - SIL, LLVM IR and assembly evidence;
- the atomic versus non-atomic A/B;
- compiler-selection tracing;
- reproduction scripts.
The tested environment documented in the repo is Apple Swift 6.3.3, Xcode 26.6, macOS 26.6.2, arm64, Swift language mode 6, with iPhone 17 Simulator used for simulator validation.
No second official Swift toolchain was tested, so this should not be presented as a permanent language rule.
References
- Swift, Actor
- Swift, Sendable
- Reproduction repository: salari-dev/swift6-arc-atomicity-lab