Will struct in Swift cause memory issue if passed around a lot?
Theoretically there could be memory concerns if you pass around very large struct
s causing them to be copied. A couple of caveats/observations:
In practice, this is rarely an issue, because we’re frequently using native “extensible” Swift properties, such as
String
,Array
,Set
,Dictionary
,Data
, etc., and those have “copy on write” (COW) behavior. This means that if you make a copy of thestruct
, the whole object is not necessarily copied, but rather they internally employ reference-like behavior to avoid unnecessary duplication while still preserving value-type semantics. But if you mutate the object in question, only then will a copy be made.This is the best of both worlds, where you enjoy value-semantics (no unintended sharing), without unnecessary duplication of data for these particular types.
Consider:
struct Foo { private var data = Data(repeating: 0, count: 8_000) mutating func update(at: Int, with value: UInt8) { data[at] = value } }
The private
Data
in this example will employ COW behavior, so as you make copies of an instance ofFoo
, the large payload won’t be copied until you mutate it.Bottom line, you asked a hypothetical question and the answer actually depends upon what types are involved in your large payload. But for many native Swift types, it’s often not an issue.
Let’s imagine, though, that you’re dealing with the edge case where (a) your combined payload is large; (b) your
struct
was composed of types that don’t employ COW (i.e., not one of the aforementioned extensible Swift types); and (c) you want to continue to enjoy value semantics (i.e. not shift to a reference type with risk of unintended sharing). In WWDC 2015 video Building Better Apps with Value Types they show us how to employ COW pattern ourselves, avoiding unnecessary copies while still enforcing true value-type behavior once the object mutates.Consider:
struct Foo { var value0 = 0.0 var value1 = 0.0 var value2 = 0.0 ... }
You could move these into a private reference type:
private class FooPayload { var value0 = 0.0 var value1 = 0.0 var value2 = 0.0 ... } extension FooPayload: NSCopying { func copy(with zone: NSZone? = nil) -> Any { let object = FooPayload() object.value0 = value0 ... return object } }
You could then change your exposed value type to use this private reference type and then implement COW semantics in any of the mutating methods, e.g.:
struct Foo { private var _payload: FooPayload init() { _payload = FooPayload() } mutating func updateSomeValue(to value: Double) { copyIfNeeded() _payload.value0 = value } private mutating func copyIfNeeded() { if !isKnownUniquelyReferenced(&_payload) { _payload = _payload.copy() as! FooPayload } } }
The
copyIfNeeded
method does the COW semantics, usingisKnownUniquelyReferenced
to only copy if that payload isn’t uniquely referenced.That’s can be a bit much, but it illustrates how to achieve COW pattern on your own value types if your large payload doesn’t already employ COW. I’d only suggest doing this, though, if (a) your payload is large; (b) you know that the relevant payload properties don’t already support COW, and (c) you’ve determined you really need that behavior.
If you happen to be dealing with protocols as types, Swift automatically employs COW, itself, behind the scenes, Swift will only make new copies of large value types when the value type is mutated. But, if your multiple instances are unchanged, it won’t create copies of the large payload.
For more information, see WWDC 2017 video What’s New in Swift: COW Existential Buffers:
To represent a value of unknown type, the compiler uses a data structure that we call an existential container. Inside the existential container there's an in-line buffer to hold small values. We’re currently reassessing the size of that buffer, but for Swift 4 it remains the same 3 words that it's been in the past. If the value is too big to fit in the in-line buffer, then it’s allocated on the heap.
And heap storage can be really expensive. That’s what caused the performance cliff that we just saw. So, what can we do about it? The answer is cow buffers, existential COW buffers...
... COW is an acronym for “copy on write”. You may have heard us talk about this before because it’s a key to high performance with value semantics. With Swift 4, if a value is too big to fit in the inline buffer, it's allocated on the heap along with a reference count. Multiple existential containers can share the same buffer as long as they’re only reading from it.
And that avoids a lot of expensive heap allocation. The buffer only needs to be copied with a separate allocation if it’s modified while there are multiple references to it. And Swift now manages the complexity of that for you completely automatically.
For more information about existential containers and COW, I’d refer you to WWDC 2016 video Understanding Swift Performance.