<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: henry</title>
    <description>The latest articles on DEV Community by henry (@_a3620048b828cbaa5d742).</description>
    <link>https://dev.to/_a3620048b828cbaa5d742</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086167%2Fc957393e-5009-4fd2-903a-d4f53b3a0708.gif</url>
      <title>DEV Community: henry</title>
      <link>https://dev.to/_a3620048b828cbaa5d742</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_a3620048b828cbaa5d742"/>
    <language>en</language>
    <item>
      <title>Swift ARC Doesn’t Scan the Heap — I Removed strong_release from SIL to Prove It</title>
      <dc:creator>henry</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:35:56 +0000</pubDate>
      <link>https://dev.to/_a3620048b828cbaa5d742/swift-arc-doesnt-scan-the-heap-i-removed-strongrelease-from-sil-to-prove-it-oed</link>
      <guid>https://dev.to/_a3620048b828cbaa5d742/swift-arc-doesnt-scan-the-heap-i-removed-strongrelease-from-sil-to-prove-it-oed</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Also available: &lt;a href="https://doing-programming.tistory.com/entry/Swift-ARC%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%ED%95%B4%EC%A0%9C%ED%95%A0%EA%B9%8C-SIL%EC%97%90%EC%84%9C-strongrelease%EB%A5%BC-%EC%A7%80%EC%9B%8C-%ED%99%95%EC%9D%B8%ED%95%B4%EB%B4%A4%EB%8B%A4" rel="noopener noreferrer"&gt;Korean version&lt;/a&gt; · &lt;a href="https://github.com/urijan44/swift-arc-sil-lab" rel="noopener noreferrer"&gt;Source code and reproducible experiments&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A source-to-runtime experiment with Swift 6.3.1: inspect ownership in SIL, delete one &lt;code&gt;strong_release&lt;/code&gt;, measure the resulting leak, compare weak-reference lowering, and follow the call stack into the Swift runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Swift ARC is sometimes explained as if it were a background service that periodically scans memory and frees objects that are no longer used. The word &lt;em&gt;Automatic&lt;/em&gt; makes that story sound plausible, but it describes a tracing garbage collector more closely than Swift’s reference-counting model.&lt;/p&gt;

&lt;p&gt;Swift ARC works differently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The compiler analyzes ownership and value lifetimes.&lt;/li&gt;
&lt;li&gt;It represents those rules with operations such as &lt;code&gt;copy_value&lt;/code&gt;, &lt;code&gt;destroy_value&lt;/code&gt;, &lt;code&gt;strong_retain&lt;/code&gt;, and &lt;code&gt;strong_release&lt;/code&gt; in SIL.&lt;/li&gt;
&lt;li&gt;Those operations are lowered to runtime behavior such as &lt;code&gt;swift_retain&lt;/code&gt; and &lt;code&gt;swift_release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When a release brings an object’s strong reference count to zero, destruction begins on that execution path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no periodic heap traversal asking which objects are still reachable from a root set. Reference-count updates are driven by ownership events.&lt;/p&gt;

&lt;p&gt;This repository checks that claim at four different layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source → SILGen → SIL → LLVM IR&lt;/li&gt;
&lt;li&gt;A causal intervention: delete exactly one object release&lt;/li&gt;
&lt;li&gt;Runtime observations: &lt;code&gt;deinit&lt;/code&gt;, maximum RSS, and an LLDB backtrace&lt;/li&gt;
&lt;li&gt;Swift’s open-source reference-counting implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also examines how &lt;code&gt;weak&lt;/code&gt; storage is lowered and uses a strong reference cycle to contrast reference counting with tracing GC.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result in one picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Swift source ownership
        │
        ▼
SIL destroy/release operations
        │
        ▼
swift_release / refcount decrement
        │
        ├── count &amp;gt; 0 ──► object stays alive
        │
        └── count == 0 ─► deinit / destruction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing the &lt;code&gt;strong_release&lt;/code&gt; for the test object broke that chain. Its &lt;code&gt;deinit&lt;/code&gt; disappeared, and a repeated-allocation workload grew from roughly 2.5 MB to 145 MB maximum RSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment environment
&lt;/h2&gt;

&lt;p&gt;The checked-in patches were verified with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apple Swift version 6.3.1
Target: arm64-apple-macosx26.0
Xcode 26.4.1
macOS 26.5.1
Runtime loaded during execution: libswiftCore.dylib 6.3.2 (6.3.2.1.3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SIL is an implementation-level representation. Instruction selection, temporary value numbers, and release placement may change between compiler versions or optimization settings. If a patch no longer applies on another toolchain, regenerate the SIL and locate the corresponding release again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short SIL primer
&lt;/h2&gt;

&lt;p&gt;SIL, the Swift Intermediate Language, sits between Swift source and LLVM IR. It preserves Swift concepts—including types, calling conventions, generics, and ownership—at a level where the compiler can reason about them before lowering to LLVM.&lt;/p&gt;

&lt;p&gt;This article observes the following pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Swift source&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.swift&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Language-level scopes, &lt;code&gt;let&lt;/code&gt;, and &lt;code&gt;weak&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw SIL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swiftc -emit-silgen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@owned&lt;/code&gt;, &lt;code&gt;@guaranteed&lt;/code&gt;, &lt;code&gt;begin_borrow&lt;/code&gt;, &lt;code&gt;destroy_value&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canonical SIL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swiftc -emit-sil -Onone -Xfrontend -disable-arc-opts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strong_release&lt;/code&gt;, &lt;code&gt;load_weak&lt;/code&gt;, &lt;code&gt;store_weak&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLVM IR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swiftc -emit-ir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;swift_release&lt;/code&gt;, &lt;code&gt;swift_weakLoadStrong&lt;/code&gt;, and other runtime entry points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Machine code/runtime&lt;/td&gt;
&lt;td&gt;Execute under LLDB&lt;/td&gt;
&lt;td&gt;The decrement-to-&lt;code&gt;deinit&lt;/code&gt; call path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;-disable-arc-opts&lt;/code&gt; is an internal compiler option used here to make ARC operations easier to inspect. It is not a recommended production build setting. With ARC optimization enabled, the compiler may remove balanced retain/release pairs, move releases, transfer ownership through calling conventions, or inline runtime operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 1: Follow a local strong reference through the compiler
&lt;/h2&gt;

&lt;p&gt;The relevant source from &lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/Examples/StrongReference.swift" rel="noopener noreferrer"&gt;&lt;code&gt;Examples/StrongReference.swift&lt;/code&gt;&lt;/a&gt; is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;TrackedObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"before scope end"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after scope"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;TrackedObject&lt;/code&gt; prints from &lt;code&gt;init&lt;/code&gt; and &lt;code&gt;deinit&lt;/code&gt;, while &lt;code&gt;consume&lt;/code&gt; is marked &lt;code&gt;@inline(never)&lt;/code&gt; so that the object has an observable use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw SIL: ownership and lifetime
&lt;/h3&gt;

&lt;p&gt;The essential part of the &lt;code&gt;-emit-silgen&lt;/code&gt; output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%37 = apply %36(...) : ... -&amp;gt; @owned TrackedObject
%38 = move_value [lexical] [var_decl] %37

%40 = begin_borrow %38
%42 = apply %41(%40) : ... (@guaranteed TrackedObject) -&amp;gt; ()
end_borrow %40

destroy_value %38
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each operation means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@owned&lt;/code&gt;: the value carries independent ownership and must be consumed exactly once along every control-flow path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;move_value [lexical]&lt;/code&gt;: establishes the lexical lifetime corresponding to the local &lt;code&gt;object&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;begin_borrow&lt;/code&gt; / &lt;code&gt;end_borrow&lt;/code&gt;: lends the object to &lt;code&gt;consume&lt;/code&gt; without transferring or duplicating ownership.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;destroy_value&lt;/code&gt;: ends the owned value’s lifetime. At this abstraction level, the compiler expresses value destruction rather than requiring a literal &lt;code&gt;swift_release&lt;/code&gt; call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/swiftlang/swift/blob/3db5a1ed8f80ec566c9e9191dc7371909e9880b0/docs/SIL/SIL.md#ownership" rel="noopener noreferrer"&gt;SIL ownership specification&lt;/a&gt; defines the same invariant: an owned value must be consumed exactly once, either by &lt;code&gt;destroy_value&lt;/code&gt; or another consuming instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canonical SIL: the concrete object release
&lt;/h3&gt;

&lt;p&gt;After ownership lowering, the same lifetime ending becomes more concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%32 = apply %31(...) : ... -&amp;gt; @owned TrackedObject
debug_value %32, let, name "object"

%35 = apply %34(%32) : ... (@guaranteed TrackedObject) -&amp;gt; ()

// Code for print("before scope end")

strong_release %32

// Code for print("after scope")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source never calls &lt;code&gt;release(object)&lt;/code&gt;. The compiler materialized &lt;code&gt;strong_release %32&lt;/code&gt; from the source-level lifetime rules.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://github.com/swiftlang/swift/blob/3db5a1ed8f80ec566c9e9191dc7371909e9880b0/docs/SIL/Instructions.md#strong_release" rel="noopener noreferrer"&gt;SIL instruction reference&lt;/a&gt; defines &lt;code&gt;strong_release&lt;/code&gt; as decrementing the referenced heap object’s strong count. If the count reaches zero, the object is destroyed; object memory is deallocated once the relevant strong and unowned counts permit it.&lt;/p&gt;

&lt;p&gt;At the LLVM IR layer, this becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight llvm"&gt;&lt;code&gt;&lt;span class="k"&gt;call&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="vg"&gt;@swift_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ptr&lt;/span&gt; &lt;span class="nv"&gt;%object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives us a traceable chain from source lifetime to a runtime reference-count operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 2: Delete one &lt;code&gt;strong_release&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/SIL/remove-object-release.patch" rel="noopener noreferrer"&gt;&lt;code&gt;SIL/remove-object-release.patch&lt;/code&gt;&lt;/a&gt; removes exactly one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  strong_release %32
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allocation, object use, print statements, destructor, and all other SIL remain unchanged. The original and modified SIL are then compiled back into separate executables.&lt;/p&gt;

&lt;p&gt;Original SIL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before scope
init(1)
consume(1)
before scope end
deinit(1)
after scope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the object’s &lt;code&gt;strong_release&lt;/code&gt; removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before scope
init(1)
consume(1)
before scope end
after scope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deinit(1)&lt;/code&gt; is gone. The scope ends and later statements execute, but the runtime does not rediscover and collect the object afterward. The ownership event that would have decremented its strong count is missing.&lt;/p&gt;

&lt;p&gt;For consistency of symbols and module context, this reproduction command passes the same module name that generated the SIL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun swiftc &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-module-name&lt;/span&gt; ARCLab &lt;span class="se"&gt;\&lt;/span&gt;
    Build/SIL/StrongReference.leaky.sil &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; Build/Binaries/strong-leaky
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Swift 6.3.1, this artifact also compiles when the module name is omitted or changed. Reusing the name is therefore a consistency choice for this experiment, not a general requirement for recompiling textual SIL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 3: Turn the missing release into measurable heap growth
&lt;/h2&gt;

&lt;p&gt;The missing &lt;code&gt;deinit&lt;/code&gt; demonstrates that the object lifetime did not end, but a memory-usage comparison makes the consequence more visible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/Examples/MemoryGrowth.swift" rel="noopener noreferrer"&gt;&lt;code&gt;Examples/MemoryGrowth.swift&lt;/code&gt;&lt;/a&gt; creates 512 &lt;code&gt;Payload&lt;/code&gt; objects. Each object owns a distinct 256 KiB array buffer. In the normal SIL, each loop iteration ends with a &lt;code&gt;strong_release&lt;/code&gt; of the local payload. &lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/SIL/remove-payload-release.patch" rel="noopener noreferrer"&gt;&lt;code&gt;SIL/remove-payload-release.patch&lt;/code&gt;&lt;/a&gt; removes only that release.&lt;/p&gt;

&lt;p&gt;Repeated verification on 2026-08-20 produced approximately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Executable&lt;/th&gt;
&lt;th&gt;Maximum RSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original SIL&lt;/td&gt;
&lt;td&gt;about 2.5 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;strong_release&lt;/code&gt; removed&lt;/td&gt;
&lt;td&gt;about 145 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The absolute values depend on the operating system, allocator, and machine state. The important property is experimental control: both executables perform the same allocations and workload, while one object release is the only intentional difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same allocation size
Same object use
Same iteration count
        │
        └── Only strong_release changes
                     │
                     ├── Present: buffers are reclaimed across iterations
                     └── Removed: buffers accumulate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a causal intervention, not merely a timing correlation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 4: What changes for a weak reference?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/Examples/WeakReference.swift" rel="noopener noreferrer"&gt;&lt;code&gt;Examples/WeakReference.swift&lt;/code&gt;&lt;/a&gt; stores its target like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;WeakObserver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WeakTarget&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getter: &lt;code&gt;load_weak&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The synthesized getter contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sil ... WeakObserver.target.getter
    : $@convention(method) (@guaranteed WeakObserver)
      -&amp;gt; @owned Optional&amp;lt;WeakTarget&amp;gt; {
bb0(%0 : $WeakObserver):
  %2 = ref_element_addr %0, #WeakObserver.target
  %3 = begin_access [read] [dynamic] %2
  %4 = load_weak %3
  end_access %3
  return %4
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ref_element_addr&lt;/code&gt; obtains the address of the &lt;code&gt;target&lt;/code&gt; property inside the observer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;begin_access [read]&lt;/code&gt; begins a read access scope for that storage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;load_weak&lt;/code&gt; safely reads the weak slot.&lt;/li&gt;
&lt;li&gt;The getter returns &lt;code&gt;@owned Optional&amp;lt;WeakTarget&amp;gt;&lt;/code&gt;. That does &lt;strong&gt;not&lt;/strong&gt; mean the field persistently owns the target. If the target is alive at load time, the operation produces a temporary strong result that the caller can use safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In LLVM IR, &lt;code&gt;load_weak&lt;/code&gt; lowers to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight llvm"&gt;&lt;code&gt;&lt;span class="nv"&gt;%target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;call&lt;/span&gt; &lt;span class="kt"&gt;ptr&lt;/span&gt; &lt;span class="vg"&gt;@swift_weakLoadStrong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ptr&lt;/span&gt; &lt;span class="nv"&gt;%weakStorage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name is descriptive: load from weak storage and, if the target is still alive, return a strong reference. This prevents a concurrent deallocation from turning the read into an invalid pointer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setter: &lt;code&gt;store_weak&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The synthesized setter contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%5 = ref_element_addr %1, #WeakObserver.target
%6 = begin_access [modify] [dynamic] %5
store_weak %0 to %6
end_access %6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The property’s persistent storage operation is &lt;code&gt;store_weak&lt;/code&gt;, not a strong-property assignment that keeps the target retained.&lt;/p&gt;

&lt;p&gt;You may still see balanced &lt;code&gt;retain_value %0&lt;/code&gt; and &lt;code&gt;release_value %0&lt;/code&gt; operations around the setter parameter. They protect temporary values during the call; they do not make &lt;code&gt;WeakObserver.target&lt;/code&gt; a persistent strong owner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Destroying weak storage
&lt;/h3&gt;

&lt;p&gt;With this toolchain, the synthesized &lt;code&gt;WeakObserver.deinit&lt;/code&gt; contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%2 = ref_element_addr %0, #WeakObserver.target
%3 = begin_access [deinit] [static] %2
destroy_addr %3
end_access %3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no literal &lt;code&gt;destroy_weak&lt;/code&gt; instruction in this canonical SIL output. The type-aware &lt;code&gt;destroy_addr&lt;/code&gt; is lowered further, and LLVM IR contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight llvm"&gt;&lt;code&gt;&lt;span class="k"&gt;call&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="vg"&gt;@swift_weakDestroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ptr&lt;/span&gt; &lt;span class="nv"&gt;%weakStorage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instruction spelling can vary by compiler stage and version. For Swift 6.3.1, the observed mapping is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Canonical SIL&lt;/th&gt;
&lt;th&gt;LLVM IR/runtime&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initialize weak storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;store_weak ... [init]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_weakInit&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assign weak storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;store_weak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_weakAssign&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read weak storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;load_weak&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_weakLoadStrong&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destroy weak storage&lt;/td&gt;
&lt;td&gt;&lt;code&gt;destroy_addr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_weakDestroy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The runtime result matches the ownership model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target init(2)
before nil: Optional(2)
target deinit(2)
after nil: nil
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For native Swift objects, forming a weak reference may create a side table. Weak variables use that side table to observe whether the target is still alive. The lifecycle comments in &lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/SwiftShims/swift/shims/RefCount.h#L54-L169" rel="noopener noreferrer"&gt;&lt;code&gt;RefCount.h&lt;/code&gt;&lt;/a&gt; describe the strong, unowned, and weak counts, along with the side-table states.&lt;/p&gt;

&lt;p&gt;One important nuance: &lt;code&gt;deinit&lt;/code&gt; and the final release of every related allocation are not necessarily the same instant. A strong count of zero begins deinitialization. Outstanding unowned references may extend the object allocation’s lifetime, and outstanding weak references may keep the side table alive even longer. That state machine is still event-driven reference counting, not tracing GC.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can we show there is no periodic traversal?
&lt;/h2&gt;

&lt;p&gt;A finite experiment cannot mathematically prove the universal negative “no hidden loop exists anywhere.” Waiting ten seconds and observing an object still alive leaves an easy objection: perhaps the hypothetical period is eleven seconds.&lt;/p&gt;

&lt;p&gt;A stronger computing argument combines evidence from independent layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Observe the compiler artifact
&lt;/h3&gt;

&lt;p&gt;The source lifetime becomes &lt;code&gt;destroy_value&lt;/code&gt;, then &lt;code&gt;strong_release&lt;/code&gt;, then &lt;code&gt;swift_release&lt;/code&gt;. This positively identifies the mechanism that initiates destruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Remove only that cause
&lt;/h3&gt;

&lt;p&gt;Deleting one &lt;code&gt;strong_release&lt;/code&gt; removes &lt;code&gt;deinit&lt;/code&gt; and changes maximum RSS from approximately 2.5 MB to 145 MB under the same workload. The result tracks the ownership event directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Inspect the runtime call stack
&lt;/h3&gt;

&lt;p&gt;Breaking on &lt;code&gt;TrackedObject.deinit&lt;/code&gt; produces this stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TrackedObject.deinit
TrackedObject.__deallocating_deinit
_swift_release_dealloc
RefCounts&amp;lt;...&amp;gt;::doDecrementSlow&amp;lt;...&amp;gt;
runStrongReferenceExperiment
main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The destructor is not entered from a collector thread or timer callback. It is reached on the main thread through the decrement path initiated by &lt;code&gt;runStrongReferenceExperiment&lt;/code&gt;. The &lt;code&gt;swift_release&lt;/code&gt; wrapper may be inlined and therefore absent as a separate frame, but &lt;code&gt;doDecrementSlow&lt;/code&gt; and &lt;code&gt;_swift_release_dealloc&lt;/code&gt; remain visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Read the runtime control flow
&lt;/h3&gt;

&lt;p&gt;The source in this experiment was compiled with Swift 6.3.1, while the executable loaded the macOS-provided &lt;code&gt;libswiftCore.dylib&lt;/code&gt; 6.3.2. The code below uses &lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/runtime/HeapObject.cpp#L548-L565" rel="noopener noreferrer"&gt;&lt;code&gt;HeapObject.cpp&lt;/code&gt; from the closest public implementation, &lt;code&gt;swift-6.3.2-RELEASE&lt;/code&gt;&lt;/a&gt;. This is not a claim that Apple’s shipped binary was built from that exact source revision; it verifies that the public control flow matches the path observed in LLDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;refCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decrementAndMaybeDeinit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The zero-count slow path in &lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/SwiftShims/swift/shims/RefCount.h#L1016-L1063" rel="noopener noreferrer"&gt;&lt;code&gt;RefCount.h&lt;/code&gt; from &lt;code&gt;swift-6.3.2-RELEASE&lt;/code&gt;&lt;/a&gt; transitions the object into deinitialization and calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;_swift_release_dealloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getHeapObject&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, &lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/runtime/HeapObject.cpp#L835-L837" rel="noopener noreferrer"&gt;&lt;code&gt;_swift_release_dealloc&lt;/code&gt;&lt;/a&gt; invokes the metadata destroy function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;asFullMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation’s control flow is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release event → decrement → zero check → destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Use a reference cycle as a distinguishing case
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/urijan44/swift-arc-sil-lab/blob/main/Examples/ReferenceCycle.swift" rel="noopener noreferrer"&gt;&lt;code&gt;Examples/ReferenceCycle.swift&lt;/code&gt;&lt;/a&gt; creates two nodes that own each other, then removes every external strong reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;
&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;

&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The objects are unreachable from the program, but each still has one strong reference from the other node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;first  --strong--&amp;gt; second
  ^                  |
  |------strong------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tracing collector reasons about reachability from roots. ARC reasons about strong reference counts. Neither count reaches zero in this cycle, so neither &lt;code&gt;deinit&lt;/code&gt; runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init(first)
init(second)
external references released
wait finished; no deinit was called
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sleep&lt;/code&gt; is not proof by itself. The stronger point is that the result can be derived by accounting for each retain and release: both final counts remain nonzero, and the runtime observation matches that calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prior art—and what this experiment adds
&lt;/h2&gt;

&lt;p&gt;This article should not claim that modifying SIL to study object deallocation is a new idea. After conducting this experiment independently, I found one particularly close precedent.&lt;/p&gt;

&lt;p&gt;In the 2023 Swift Forums thread &lt;a href="https://forums.swift.org/t/sil-strong-release-behavior/66387" rel="noopener noreferrer"&gt;“SIL strong_release behavior”&lt;/a&gt;, the author generated SIL, removed the &lt;code&gt;dealloc_ref&lt;/code&gt; inside &lt;code&gt;__deallocating_deinit&lt;/code&gt;, and observed a leak with Apple’s &lt;code&gt;leaks&lt;/code&gt; tool. Joe Groff explained that reaching a strong count of zero invokes &lt;code&gt;__deallocating_deinit&lt;/code&gt;; that function is then responsible for invoking &lt;code&gt;deinit&lt;/code&gt;, destroying stored properties, and deallocating the object’s memory.&lt;/p&gt;

&lt;p&gt;That experiment and this repository modify different links in the same lifecycle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;2023 Swift Forums experiment&lt;/th&gt;
&lt;th&gt;This experiment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Removes &lt;code&gt;dealloc_ref&lt;/code&gt; inside the deallocating destructor&lt;/td&gt;
&lt;td&gt;Removes the earlier &lt;code&gt;strong_release&lt;/code&gt; ownership event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observes the result with &lt;code&gt;leaks&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compares &lt;code&gt;deinit&lt;/code&gt; output and maximum RSS: ~2.5 MB → 145 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Focuses on strong-reference deallocation&lt;/td&gt;
&lt;td&gt;Compares strong and weak lowering in SIL and LLVM IR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explains the SIL destructor relationship&lt;/td&gt;
&lt;td&gt;Captures &lt;code&gt;doDecrementSlow → _swift_release_dealloc → deinit&lt;/code&gt; in LLDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Investigates one surprising result&lt;/td&gt;
&lt;td&gt;Packages the hypothesis as a reproducible source-to-runtime repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is also a direct historical confirmation of the “no explicit periodic GC phase” model. In the 2016 Swift compiler discussion &lt;a href="https://forums.swift.org/t/questions-about-arc/4621/6" rel="noopener noreferrer"&gt;“Questions about ARC”&lt;/a&gt;, a participant asked whether Swift used immediate reference counting without an explicit garbage-collection phase. &lt;a href="https://forums.swift.org/t/questions-about-arc/4621/7" rel="noopener noreferrer"&gt;Swift developer Roman Levenstein confirmed that understanding&lt;/a&gt;. The same thread notes that ARC updates can still be coalesced or optimized; that does not turn the runtime into a tracing collector.&lt;/p&gt;

&lt;p&gt;The contribution here is therefore not the first observation that SIL controls object destruction. It is the combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;directly removing the &lt;code&gt;strong_release&lt;/code&gt; that ends the object lifetime,&lt;/li&gt;
&lt;li&gt;measuring the same workload with and without that release,&lt;/li&gt;
&lt;li&gt;explaining weak storage across SIL and LLVM IR,&lt;/li&gt;
&lt;li&gt;capturing the runtime decrement-to-destructor stack, and&lt;/li&gt;
&lt;li&gt;publishing the complete process as a reproducible experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Important limitations and common traps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The exact source line for &lt;code&gt;deinit&lt;/code&gt; is not fixed
&lt;/h3&gt;

&lt;p&gt;Event-driven reference counting does not imply that a release must remain at the closing brace shown in source. The optimizer may shorten a lifetime after its last use, subject to lexical-lifetime and deinit-barrier rules. Use &lt;code&gt;withExtendedLifetime(_:)&lt;/code&gt; when a lifetime must be explicitly extended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retains and releases do not always survive as function calls
&lt;/h3&gt;

&lt;p&gt;The compiler may eliminate balanced operations, transfer ownership through calling conventions, or inline runtime functions. Do not expect every source reference to produce a visible one-to-one &lt;code&gt;swift_retain&lt;/code&gt; / &lt;code&gt;swift_release&lt;/code&gt; pair in final assembly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;autoreleasepool&lt;/code&gt; is not tracing GC
&lt;/h3&gt;

&lt;p&gt;Objective-C interoperability can delay releases for autoreleased objects until a pool drains. That batches registered ownership operations; it does not scan the heap for unreachable objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weak references are not free
&lt;/h3&gt;

&lt;p&gt;A weak field does not persistently increment its target’s strong count, but weak storage still requires runtime bookkeeping, side-table state, and safe concurrent loads. “Does not own” is not the same as “has no runtime cost.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the experiments
&lt;/h2&gt;

&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;li&gt;Xcode command-line tools&lt;/li&gt;
&lt;li&gt;a Swift toolchain close enough to the verified version for the textual SIL patches to apply&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./Scripts/run-experiments.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates raw SIL, canonical SIL, and LLVM IR under &lt;code&gt;Build/SIL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Applies the patches that remove the selected &lt;code&gt;strong_release&lt;/code&gt; operations.&lt;/li&gt;
&lt;li&gt;Compiles the original and modified SIL into separate executables.&lt;/li&gt;
&lt;li&gt;Runs the strong, weak, and cycle experiments.&lt;/li&gt;
&lt;li&gt;Reports maximum RSS for the repeated-allocation workload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Capture the release-to-&lt;code&gt;deinit&lt;/code&gt; call stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./Scripts/capture-release-stack.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search the generated artifacts directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'destroy_value|strong_release'&lt;/span&gt; Build/SIL/StrongReference.&lt;span class="k"&gt;*&lt;/span&gt;.sil
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'load_weak|store_weak'&lt;/span&gt; Build/SIL/WeakReference.&lt;span class="k"&gt;*&lt;/span&gt;.sil
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'swift_release|swift_weak'&lt;/span&gt; Build/SIL/&lt;span class="k"&gt;*&lt;/span&gt;.ll
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;Automatic&lt;/em&gt; in Swift ARC does not mean that the runtime periodically walks the heap. It means developers do not manually write every retain and release: the compiler derives ownership operations from the program’s lifetime rules, and the runtime applies the resulting reference-count state transitions.&lt;/p&gt;

&lt;p&gt;In these experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source ownership became SIL destruction and release operations.&lt;/li&gt;
&lt;li&gt;Removing one &lt;code&gt;strong_release&lt;/code&gt; removed &lt;code&gt;deinit&lt;/code&gt; and caused measurable heap growth.&lt;/li&gt;
&lt;li&gt;LLDB connected the decrement slow path directly to the deallocating destructor.&lt;/li&gt;
&lt;li&gt;Weak properties lowered through dedicated weak-storage operations and side-table runtime calls.&lt;/li&gt;
&lt;li&gt;A strong cycle remained allocated because its reference counts never reached zero, even after the objects became unreachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the right mental model for Swift ARC: not periodic cleanup, but compiler-generated ownership operations driving runtime reference-count transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/" rel="noopener noreferrer"&gt;The Swift Programming Language — Automatic Reference Counting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/swiftlang/swift/blob/3db5a1ed8f80ec566c9e9191dc7371909e9880b0/docs/SIL/SIL.md" rel="noopener noreferrer"&gt;Swift SIL specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/swiftlang/swift/blob/3db5a1ed8f80ec566c9e9191dc7371909e9880b0/docs/SIL/Instructions.md#strong_release" rel="noopener noreferrer"&gt;Swift SIL instruction reference — &lt;code&gt;strong_release&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/runtime/HeapObject.cpp" rel="noopener noreferrer"&gt;Swift 6.3.2 runtime — &lt;code&gt;HeapObject.cpp&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/swiftlang/swift/blob/swift-6.3.2-RELEASE/stdlib/public/SwiftShims/swift/shims/RefCount.h" rel="noopener noreferrer"&gt;Swift 6.3.2 runtime — &lt;code&gt;RefCount.h&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forums.swift.org/t/sil-strong-release-behavior/66387" rel="noopener noreferrer"&gt;Swift Forums — SIL strong_release behavior (2023)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forums.swift.org/t/questions-about-arc/4621/7" rel="noopener noreferrer"&gt;Swift Forums — Questions about ARC (2016)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>memorymanagement</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
