Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Will struct in Swift cause memory issue if passed around a lot?

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?

like image 375
johndoe Avatar asked Feb 25 '19 12:02

johndoe


People also ask

Are structs stored in memory?

Structs and classes are data types, so they don't occupy memory at runtime. Structs and classes are really compile-time things, not runtime things. Objects (variables) of those data types do occupy memory.

Which is faster class or struct Swift?

Classes are reference types, and structs are value types. If class inheritance is not needed, structs are faster and more memory efficient. Use structs for unique copies of an object with independent states. Use structs when working with a few, relatively simple data values.

Is struct thread safe Swift?

Structs are thread-safe Since structs are unique copies, it's only possible for one thread at a time to manage them under most circumstances. This prevents them from being accessed by more than one thread simultaneously which can prevent difficult-to-track bugs.


2 Answers

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.

like image 77
Rob Avatar answered Nov 15 '22 08:11

Rob


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

like image 24
Mohmmad S Avatar answered Nov 15 '22 08:11

Mohmmad S