Large Struct COW optimization

Hello,

Recently I found myself forced to think about the performance issues which COW for structs might introduce in my code. To put the problem into context I will try to explain the situation in greater detail (despite some of those details are not really relevant to the problem, they will give some better sense to the context). Specifically, I have some business layer DTO entity modeled using a struct which has to be mapped to another corespoding struct model. In order to map it, the initialization needs some information contained in a different entity. Take the following example:

struct DestinationDTO {
    let id: String
}

struct MeetingDTO {
    let id: String
}

struct Meeting {
    let id: String
    let destinationId: String
    
    init(meeting: MeetingDTO, destinations: [DestinationDTO]) {
        self.id = meeting.id
        ...
    }
}

let destinations = [DestinationDTO(id: "1"), DestinationDTO(id: "2")])

let meeting = Meeting(meeting: MeetingDTO(id: "21"), destinations: destinations]

Worth mentioning is that Meeting only needs one element from destinations, based on destinationId.

Now as I understand from the Swift documentation, value types are copied when passed as function arguments which means destinations is presumably copied when passed to the Meeting initializer. I shall continue by saying that for my particular case the corresponding DestinationDTO struct is one very large. Moreover this case also occurs one level deeper, where another field of Meeting is initialized in the same fashion using a field from the corresponding (by destinationId) destination .

Nevertheless, also mentioned in the Swift documentation is:

Collections defined by the standard library like arrays, dictionaries, and strings use an optimization to reduce the performance cost of copying. Instead of making a copy immediately, these collections share the memory where the elements are stored between the original instance and any copies. If one of the copies of the collection is modified, the elements are copied just before the modification. The behavior you see in your code is always as if a copy took place immediately.

Provided the destinations collection is not modified during the initialization, can I resolve this to: the same copy of destinations is used for each initialization ? (if multiple are required at call site)

Thank you.

Yes, that should share the underlying Array and String storage.

1 Like

Strictly speaking, it won't even be able to become modified during the initialization if you don't pass it as inout (which, it seems, you don't do), as function argument are effectively let-constants. It is totally possible to copy that array into a new var within the body of the function and start modifying it, though, which will trigger COW.

In Copy-on-Write, the important thing to understand is that copy occurs on write. That is, only when you call a mutating function, the underlying storage will (potentially) become copied. Otherwise, what's being copied when you pass any array to a function (regardless how large the elements are) is effectively a pointer to the buffer of the array, which is just 8 bytes.

1 Like

Hey thanks, sounds good.

This means that even though, say inside the initializer, I would do something like:

let meetingDestination = destinations.first { $0.id == destinationId }

meetingDestination still does not trigger COW as long as it is just read?

Correct. You'd need to a) mutate it in some way, while b) more than one variable was sharing the storage.

1 Like

Fair enough, thank you ! :fist_right: :fist_left:

Some minor pedantry:

it's not meetingDestination = ... assignment itself that doesn't trigger COW, it's the fact that you don't call any mutating function on the variable. If you were to declare meetingDestination as a var or store it as a var somewhere else (maybe inside an instance variable of the Meeting struct you have), then it would be possible to trigger COW later (i.e., outside the initializer), perhaps much later. So, if you care about the copy performance, you have to be careful not to mutate the variable or a copy of it or a copy of a copy of it.

This talk sheds much more light on the issue!

1 Like
Terms of Service

Privacy Policy

Cookie Policy