9

In Swift structs are value types. If I have a struct that hold a large data (hypothetically) and I pass the struct to many different functions, will the struct be duplicated each time? If I call it concurrently then memory consumption would be high right?

John Doe
  • 2,225
  • 6
  • 16
  • 44
  • Structs are passed by value so a copy is made each time, yes. – Magnas Feb 25 '19 at 12:13
  • 3
    Swift can optimise by specialising functions that decompose properties of structs in addition to passing structs by reference, compare https://stackoverflow.com/q/43486408/2976878. I wouldn’t worry about passing large structs around unless you’ve specifically identified that as a performance issue in your code through profiling. – Hamish Feb 25 '19 at 13:07

3 Answers3

9

Theoretically there could be memory concerns if you pass around very large structs causing them to be copied. A couple of caveats/observations:

  1. 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 the struct, 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 of Foo, 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.

  2. 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, using isKnownUniquelyReferenced 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.

  3. 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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Does that mean that for a lot of copy actions for small structs memory consumption would still grow? – pckill Feb 25 '19 at 14:52
  • so its kinda ref type 'till its mutated ? – Mohmmad S Feb 25 '19 at 14:58
  • @Mohmmad - yes, you can think of it as reference-like behavior until mutated. Not quite the same, of course, but conceptually similar. – Rob Feb 25 '19 at 15:07
  • @Rob but isn't structs live in the stack memory ? https://stackoverflow.com/a/39180867/8192960 – Mohmmad S Feb 25 '19 at 15:08
  • i am a bit confused, could you please go on with me i am trying to figure this out .. considering this is the right answer for sure, but does that mean that the structs never gets deallocated ? they just share the same buffer lets say if they identical ? and considering the case that a mutating could happen while copying them does all of those copies live in the buffer along side with the ref ? and how does that affect the memory if so – Mohmmad S Feb 25 '19 at 15:16
  • I’d refer you to WWDC 2016 video [Understanding Swift Performance](https://developer.apple.com/videos/play/wwdc2016/416) that goes into existential containers and CoW in more detail. Or search Stack Overflow for [swift "copy on write"](https://stackoverflow.com/search?q=%5Bswift%5D+%22copy+on+write%22) as this has been discussed frequently here, too. But, obviously structs do get deallocated. That shared buffer employs a reference-counting sort of structure and gets deallocated when there are no more value types using it. – Rob Feb 25 '19 at 17:19
  • @Hamish - You’re absolutely right. Thanks for pointing that out. I’ve tried to clarify my answer. – Rob Feb 26 '19 at 21:26
  • @Rob No worries! It's also worth noting that the compiler [can perform a variety of optimisations to avoid copying](https://stackoverflow.com/a/43493749/2976878) large structures around such as specialising functions to only take the parts of the struct they need as parameters and generating functions that take large struct parameters by reference (and then copying only when they need to). – Hamish Feb 27 '19 at 17:34
1

Yes. it will, if they're in the same scope, because structs get deallocated after they run out of scope, so they are deallocated with whatever base class they live in, however having too many in the scope with the same value could add up to make a problem for you so be careful, also this is a great article that talks about those topics in details.

Also you can't put a deinit in a struct to look at this directly, but there is a workaround. You can make a struct that has a reference to a class that prints something when deallocated, like this:

class DeallocPrinter {
    deinit {
        print("deallocated")
    }
}

struct SomeStruct {
    let printer = DeallocPrinter()
}

func makeStruct() {
    var foo = SomeStruct()
}
makeStruct() // deallocated becasue it escaped the scope

Credits

Mohmmad S
  • 5,001
  • 4
  • 18
  • 50
0

This really depends on two main factors: the amount of times your struct is passed around, and how long structs are 'kept alive' (a.k.a are they also quickly cleaned up by ARC?).

The total amount of memory consumption can be calculated using: mem_usage = count * struct_size

where count is the total amount of structs that are 'alive' at any given moment. You need to make a judgement for yourself if the structs remain alive or are cleaned up quickly.

ImJustACowLol
  • 826
  • 2
  • 9
  • 27