Copy-On-Assignment vs. Copy-On-Write in Swift
Understanding memory management has always been an important topic to avoid memory performance issues and crashes. A good way to understand something is to ask questions about how it works. This article is answering a question “How memory is managed during copying an object?”. Also, going further, to manage the memory in a smart and efficient way.
“Copy-on-assignment” and “Copy-on-write” are two important mechanisms that happen under the hood when copying objects. It gives you a control to use the memory in a better and efficient way.
Copy-on-assignment is the default behavior when creating two struct objects and assigning one of them to the other. It means a full copy of the object is created when assigning it to another object, it copies not only its content but also the remote resources that are referenced within it.
You may think it is expensive to fully copy the object during the assignment. In fact it depends on what you exactly copy. For example, a struct object — that doesn’t have any reference type inside it — is fully stack allocated without any reference count overhead; therefore, copy-on-assignment for a fully stack allocated object is considered fast and not expensive.
In the example below, assigning one struct object to the other creates a full copy with two different addresses for each object.
On the other hand, a struct object with a reference type (e.g.class object) inside it is not considered a fully stack allocated object because it has references that point to the heap; therefore, copying this object is expensive because heap allocation is slower and it is shared, which requires thread safety.
Copy-on-write is an optimization that is added to all variable-size collections like Array, Dictionary and Set in the standard library.
Copies of variable-size object share the same storage until one of the copies is modified.This means assigning object to another will not copy the value-type (e.g. struct) but creates a reference until one of the copies is modified.
Coping nested data structures could be time-consuming and hurts the performance and the memory of the program. Therefore, the example below uses copy-on-write to perform better during copying objects.
This code represents a Maze struct which is a simple data structure that has a root Path (rootElement) of type class, every Path could have nested paths (left, right).
Let’s create a test function and trace the code step by step …
Step 1: Create the object
maze1 object is created and allocated in the stack because the struct object is a value-type object. rootElement property is a class object; therefore, it is allocated in the heap and the stack has a pointer that points to it. This can be shown in the figure below.
2- Copy the object
maze2 object is created as a copy of maze1 and it is allocated in the stack. rootElement property is not duplicated because copying value types only increases its reference-count; therefore, comparing the two addresses of the rootElement objects shows that it is the same.
3- Make a change to the object
Copy-on-write is used to fully copy the rootElement Property, when setting a new value to value property, check the reference count of the rootElement at the setter code , If rootElement is not uniquely referenced then create a new object of the root element, if rootElement is uniquely referenced then there is no need to create a new object.
The above code comparing the two addresses of the rootElement objects shows that they are different addresses.
Copy-on-write and copy-on-assignments are two different and important mechanisms that can happen during copying an object. Copy-on-assignment is considered fast and not expensive when copying a fully stack allocated object. Copy-on-write is usually used with complex data structures that are not fully stack allocated; you need to implement it yourself to determine when the full copy is created.
The most important thing is to understand the usage of the object and how it is allocated in the memory. This will help you to determine which approach can be used to achieve the best usage of the memory.