Soon after the publication of the Swift source code, I wrote an article
about how weak links are implemented . Time does not stand still and everything changes, the implementation of weak links in Swift too. Today I will talk about the new implementation and compare it with the old one. Thank you Guillaume Lessard for the idea for the post.
Old implementation
For those of you who do not remember the old implementation and who are too lazy to run through the previous article, I will briefly describe it here.
In the old version of Swift, objects had 2 reference counters: a counter for strong links and a counter for weak ones. When the strong link counter becomes zero, and there are no weak links yet, the object is destroyed, but the memory is not released. Therefore, the memory remains the so-called “zombie object” which refers to a weak link.
')
When a weak link is loaded, the runtime environment checks if the object is a zombie. If it is a “zombie”, then runtime resets a weak link and decreases the weak link counter to 1. When the weak links counter reaches 0, the memory is released (deallocated). This means that the object is completely removed only when all weak references to it are reset.
I liked the simplicity of this implementation, but it also had its drawbacks:
- Zombie objects could remain in memory for a long time. Classes with a large number of properties (property) in this case can consume a lot of memory.
- Another problem that I discovered after writing the article was that the implementation was not thread safe for simultaneous reading. Oops! This has been corrected, but the discussion surrounding this problem raised the question of creating a new implementation of weak links that is more resistant to similar problems.
Object data
An object in Swift consists of many pieces of data.
Firstly , it is obvious that it contains all the properties (properties) that are declared in the source code of the class. This is what is directly described by the programmer.
Secondly , it refers to the class object. This is needed for dynamic dispatch (what is called virtual methods in C ++) and for implementing the built-in function “type (of :)” to determine the type of an object at runtime. This is largely hidden from the programmer, although the virtual methods and the presence of the “type (of :)” function) itself suggest this mechanism.
Thirdly , it contains reference counters. They are completely hidden from you if you do not resort to dirty hacks such as reading the “raw memory” of an object, or using compiler settings to call CFGetRetainCount.
Fourth , you have additional information stored for an Objective-C runtime, such as a list of all weak references and associated objects. (The implementation of weak links for Objective-C keeps track of each link separately)
Where is all this data stored?
In Objective-C, a pointer to a class (isa) and properties (properties) are stored directly in the memory of the object. A pointer to a class object occupies the first piece of memory, followed by instance variables (ivar). Additional information is stored in external tables. When you need an associated object, the runtime finds it in a large hash table, the keys of which are the addresses of the objects. This works slowly and requires the object to be captured (locked) to synchronize access to it. The reference count is sometimes stored in the memory of the object, and sometimes in an external table, depending on the OS version and processor architecture.
Abstracting from how this is implemented, let's ask ourselves the question: how should this be implemented?
Each method of location in the memory has its pros and cons. You can quickly access data that is stored inside the memory of an object, but this consumes additional memory. Data access in external tables takes more time, but does not consume additional memory for objects that do not need them. This is one of the reasons why Objective-C did not initially store the reference count directly in the object. Objective-C was developed at a time when computers were severely memory limited. Most objects in typical Objective-C code have only one owner, you can imagine that their counter is 1. To allocate 4 bytes to each object that stores 1 all the time would be waste. In the external table, we can replace the total value of 1 with no record at all, which will save memory.
Each object is associated with a class object. Every method call needs it.
Therefore, a pointer to the class is stored in the object itself, it is not beneficial to do it otherwise.
Property access is expected to be fast. Their presence is determined at the compilation stage. Objects without properties may not allocate memory for them, even if they are usually stored in object memory.
Side Tables
The new implementation of Swift introduced the concept of a side table.
A side table is an additional memory area in which additional information about the object is stored. It is optional, which means that it will not necessarily be present at the object. Objects that need it suffer some losses in performance, and those that do not need it do not experience its “load”.
Each object has a pointer to its side table, just as a side table has a pointer to an object. Side tables can store additional information — such as an associated object. To avoid additional costs in the form of 8 bytes per pointer to a side table, Swift resorts to elegant optimization. Initially, the first field in the object stores a pointer to the class, and the second is the reference count. When an object needs a third-party table, this second field begins to be used to store a pointer to it. Since, in any case, the object needs reference counters, they are now located in a third-party table. To distinguish between these two cases, a certain bit is reserved in the second field, which determines whether the field stores a reference count or a pointer to a side table.
Thanks to the side tables, Swift implements the old reference counting system while eliminating its drawbacks. Instead of pointing to the object itself, weak links now point to a side table. Since side tables take up little space, we get rid of the problem of memory waste due to weak references to large objects. This leads to a simple solution to the problem of thread safety: do not reset weak links. Since the side tables are small in size, we can keep weak references to it until the links themselves are overwritten or destroyed.
I should mention that the current implementation of side tables contains only reference counters and a pointer to the object itself. Additional applications, such as the implementation of an associated object for Swift, remain hypothetical. Swift does not yet have built-in functionality for the associated object, and Objective-C still uses global tables.
The described approach has great potential and we may soon see the associated objects in Swift. I hope that this will make it possible to store properties in class extensions and other cool features.
Code
Since the Swift code is open, it is possible to see everything described above clearly.
Everything related to side tables .
A high-level weak link interface with full comments about the system can be
found here .
Implementations and comments about objects placed on the heap can be
found here .
Links refer to specific commit versions so that people who read this in the future have an idea of what I am describing. If you are looking for the latest implementation, be sure to switch to the master or update the repository after clicking on the link.
Conclusion
Weak links are an important feature of Swift. The initial implementation was simple and clean, but had some problems. By adding optional side tables, Swift developers were able to get rid of these problems, while retaining the merits of the previous implementation. It also provides opportunities for adding new cool features in the future. That's all for today, please send your ideas for posts if you want me to highlight them.
From the translator:If you notice some kind of error (technical or not), please write in the comments about this.
Thank.