Unveiling Swift’s Array: Memory Allocation, SIL Insights, and Copy‑On‑Write Mechanics
This article dives deep into Swift's Array implementation, explaining how the language allocates memory, the role of SIL and internal structs like _Array, _ArrayBody, and _ContiguousArrayStorage, and how copy‑on‑write ensures efficient mutation handling, all illustrated with real code snippets and debugger output.
Background
In a previous article I mentioned a crash caused by iterating over an Array while mutating it. The crash log shows that a mutable array was mutated during enumeration, which Swift forbids because Array is not thread‑safe.
To understand how Swift implements Array, I created a simple command‑line project and inspected the generated SIL (Swift Intermediate Language) and source code.
Array Definition
Arrayin Swift is a struct. Its definition in Array.swift looks like this:
@frozen
public struct Array<Element>: Swift._DestructorSafeContainer {
#if _runtime(_ObjC)
@usableFromInline internal typealias _Buffer = _ArrayBuffer<Element>
#else
@usableFromInline internal typealias _Buffer = _ContiguousArrayBuffer<Element>
#endif
@usableFromInline internal var _buffer: _Buffer
@inlinable internal init(_buffer: _Buffer) { self._buffer = _buffer }
}The struct contains a single stored property _buffer, which is either an _ArrayBuffer or a _ContiguousArrayBuffer depending on the runtime.
Creating an Array and SIL Generation
I wrote the following Swift code:
var num: Array<Int> = [1, 2, 3]
withUnsafePointer(to: &num) {
print($0)
}
print("end")Compiling with
swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.silproduced SIL that shows the array being created via @Swift._allocateUninitializedArray<A>(). This intrinsic returns a tuple containing the newly allocated array and a raw pointer to its storage.
Allocation Path
The allocation function calls Builtin.allocWithTailElems_1, which eventually invokes swift::swift_allocObject to reserve heap memory for a _ContiguousArrayStorage instance.
HeapObject *swift::swift_allocObject(HeapMetadata const *metadata, size_t requiredSize, size_t requiredAlignmentMask) {
CALL_IMPL(swift_allocObject, (metadata, requiredSize, requiredAlignmentMask));
}The allocated storage is then adopted by the array via Array._adoptStorage:
internal static func _adoptStorage(_ storage: __owned _ContiguousArrayStorage<Element>, count: Int) -> (Array, UnsafeMutablePointer<Element>) {
let innerBuffer = _ContiguousArrayBuffer<Element>(count: count, storage: storage)
return (Array(_buffer: _Buffer(_buffer: innerBuffer, shiftedToStartIndex: 0)), innerBuffer.firstElementAddress)
}The innerBuffer is a _ContiguousArrayBuffer that holds a reference to the storage and initializes its header:
internal init(count: Int, storage: _ContiguousArrayStorage<Element>) {
_storage = storage
_initStorageHeader(count: count, capacity: count)
}Buffer Header
The header is an _ArrayBody struct containing a _SwiftArrayBodyStorage with count and _capacityAndFlags. The capacity is stored as (capacity << 1) | elementTypeIsBridgedVerbatim, so the actual capacity is retrieved by shifting right one bit.
internal init(count: Int, capacity: Int, elementTypeIsBridgedVerbatim: Bool = false) {
_storage = _SwiftArrayBodyStorage(
count: count,
_capacityAndFlags: (UInt(truncatingIfNeeded: capacity) << 1) |
(elementTypeIsBridgedVerbatim ? 1 : 0))
}
internal var capacity: Int { return Int(_capacityAndFlags >> 1) }First Element Address
The buffer provides a pointer to the first element using the builtin projectTailElems operation:
internal var firstElementAddress: UnsafeMutablePointer<Element> {
return UnsafeMutablePointer(Builtin.projectTailElems(_storage, Element.self))
}This pointer points to the memory immediately after the buffer’s header, where the actual array elements reside.
Copy‑On‑Write (COW) Mechanics
Swift arrays use copy‑on‑write to avoid unnecessary copying. When a mutating operation such as append is called, the implementation first checks whether the underlying storage is uniquely referenced:
internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() {
if !_buffer.beginCOWMutation() {
_createNewBuffer(bufferIsUnique: false, minimumCapacity: count + 1, growForAppend: true)
}
}The uniqueness test examines the strong reference count of the storage; a count of zero means the array has the sole reference.
return !getUseSlowRC() && !getIsDeiniting() && getStrongExtraRefCount() == 0;If the storage is shared, a new buffer is allocated, and the elements are either moved (if the original buffer was unique) or copied:
if bufferIsUnique {
let dest = newBuffer.firstElementAddress
dest.moveInitialize(from: mutableFirstElementAddress, count: c)
_native.mutableCount = 0
} else {
_copyContents(subRange: 0..<c, initializing: newBuffer.mutableFirstElementAddress)
}Thus, the COW behavior hinges on the heap‑allocated storage’s reference count.
Verification
Running the sample code and inspecting memory with LLDB confirms that the Array variable holds a pointer to a _ContiguousArrayStorage object, and that the storage’s reference count changes as expected when the array is copied and mutated.
Conclusion
Although Array is a Swift struct, its elements live on the heap inside a _ContiguousArrayStorage. Swift’s copy‑on‑write optimization relies on the storage’s strong reference count: a count of zero indicates unique ownership, allowing in‑place mutation; otherwise a new buffer is allocated and the data is copied.
References
NSMutableArray原理揭露: http://blog.joyingx.me/2015/05/03/NSMutableArray%20%E5%8E%9F%E7%90%86%E6%8F%AD%E9%9C%B2/
探索Swift中Array的底层实现: https://juejin.cn/post/6931236309176418311
Sohu Smart Platform Tech Team
The Sohu News app's technical sharing hub, offering deep tech analyses, the latest industry news, and fun developer anecdotes. Follow us to discover the team's daily joys.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
