-
-
Notifications
You must be signed in to change notification settings - Fork 7
Description
CPython
In CPython they use manual reference counting:
- each object has a reference count and an address of the previous and the next allocated object (doubly linked list)
Py_INCREFis used to retain (reference count += 1)Py_DECREFto release (reference count -= 1) an object
If the reference count reaches 0 then it is deallocated and the linked list entries are updated (having a reference to both previous and next object makes this trivial). Linked list itself is used by garbage collection algorithm to break cycles (at some point they need to iterate over all of the currently alive objects).
This is how the object header looks like:
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;Violet
In Swift we have automatic reference counting for class instances (and a few others). The big catch here is that the user (programmer) is expected to take care of the strong reference cycles (to simplify things a bit: imagine 2 objects that hold a reference to each other, both have reference count 1, so neither of them will be deallocated).
Our options are:
-
manual
retain/release- in this approach the programmer is responsible for inserting theretain/releasecalls. For example: when adding an object to alistweretainit, when removing it (or deleting the wholelist) wereleaseit.The main drawback is of course the manual labor of adding the
retain/releasecalls. It is also extremely difficult to get right and even a single mistake may have consequences:- unbalanced
retain- object lives forever (along with all of the objects it references) - unbalanced
release- “use after free” error -> crash
This approach is really good if you can make it right. CPython uses it, because they can afford it: they have the time and manpower to find and fix any possible errors. I don't think we could do the same.
- unbalanced
-
manually implemented smart pointer - I don't think it is possible in Swift to write our own version of smart pointer. Even in languages which give you more control (like C++) it is an extremely hard thing to do.
-
full garbage collection without ARC - there are tons of materials about this on the internet.
-
(THIS ONE IS NOT POSSIBLE ACCORDING TO NEW OBJECT MODEL) using Swift native ARC - I'm not really sure if you can just allocate a bunch of memory with native Swift ARC, but as a last resort we can use ManagedBufferPointer without elements.
This is nice because (in theory) Swift takes case of everything. In practice however… there are some problems. First of all, you still need a some form of garbage collection to break the cycles. This will be simpler than full-on garbage collection and the requirements will be not as demanding since it will run less often, but still, it is something to keep in mind.
The other things is that most of the garbage collection algoritms work by somehow marking all of the reachable objects and then in a 2nd phase removing all of the not-marked ones. Unfortunately to do this you need a reference to all of the reachable objects (most commonly by some sort of a linked list). The question is: what kind of reference would it be?
- strong - always present +1 reference count, which invalidates the whole point of reference counting
unowned- this will allow objects to be deallocated, but we would somehow need to know which references are alive and which notweak- this is an obvious choice
Unfortunately there is a possible performance problem since having just a single the
weakreference in Swift, moves the whole reference count to side-table. Unfortunately this side-table is off-line (it is an separate allocation outside of the object). This is a potential memory/cache waste, not to mention that the retain now has to fetch the object and then fetch the side-table (it is calledslowRCfor a reason).Code to test the presence of the side table
import Foundation // Sources: // SWIFT_REPO/stdlib/public/SwiftShims/RefCount.h // SWIFT_REPO/stdlib/public/SwiftShims/HeapObject.h let strongExtraRefCountShift: UInt64 = 33 let strongExtraRefCountBitCount: UInt64 = 30 let strongExtraRefCountMask: UInt64 = ((1 << strongExtraRefCountBitCount) - 1) << strongExtraRefCountShift let useSlowRCShift: UInt64 = 63 let useSlowRCBitCount: UInt64 = 1 let useSlowRCMask: UInt64 = ((1 << useSlowRCBitCount) - 1) << useSlowRCShift func printARC(name: String, ptr: UnsafeMutableRawPointer) { let namePad = name.padding(toLength: 18, withPad: " ", startingAt: 0) // let typePtr = ptr.load(as: UInt64.self) let refCountBits = ptr.load(fromByteOffset: 8, as: UInt64.self) let hasSideTable = (refCountBits & useSlowRCMask) >> useSlowRCShift if hasSideTable == 1 { print("\(namePad)| sideTable") } else { let strongExtraRefCount = (refCountBits & strongExtraRefCountMask) >> strongExtraRefCountShift print("\(namePad)| strongExtraRefCount: \(strongExtraRefCount)") } } class Alice { var ptr: UnsafeMutableRawPointer { return Unmanaged.passUnretained(self).toOpaque() } } let alice = Alice() printARC(name: "1 strong", ptr: alice.ptr) // strongExtraRefCount: 0 let aliceInWonderland = alice printARC(name: "2 strong", ptr: alice.ptr) // strongExtraRefCount: 1 weak var aliceBackInLondon = alice printARC(name: "2 strong + 1 weak", ptr: alice.ptr) // sideTable
Btw. we had a similar problem when writing our implementation of BigInt: after the heap-allocated storage is not needed -> how do we release it? And how do we even know that it is no longer needed? We went with ManagedBufferPointer to get Swift ARC, but technically you can implement an BigInt with a garbage collector (although I am not sure that this would be a good idea).
I think that the main difference between the BigInt and the Python object representation is that the Python objects can contain references to other objects, possibly creating a cycle. Anyway, under the hood we were dealing with an unowned pointers, so we can either find the owners (this would be on case-by-case basis, so a lot of room for mistakes) or automate things (either via ARC or garbage collector).