Time for another deep dive into CPython internals to learn how it implements memory management. CPython primarily uses reference counting (RC) for memory management.
RC is a light-weight technique for managing memory. Each object internally maintains a count of references pointing to it. On instantiation, an object starts with a ref count of one and as on each assignment to a new variable, its ref count gets incremented by one. Similarly, as each variable goes out of scope, the object's ref count goes down by one. When the object's ref count reaches 0, it gets freed because it is no longer used.
Now coming to the implementation details, in CPython, every object has a header called PyObject. This header contains the ref count of the object and the object's type details. It is defined in the file Includes/object.h as the struct _object.
Next, how these references are updated. Does every one directly updated the ob_refcnt field or is there a better way? Well, there are functions provided to do this and everyone should call them instead of doing it themselves. These are called Py_INCREF and Py_DECREF, defined in Includes/object.h.
The definition of Py_INCREF and Py_DECREF is very similar. The main difference being that Py_DECREF also deallocates the object if its ref count reaches 0 after the decrement operation.
But, where and when exactly does CPython call these functions to update the ref counts? Well, the ref counts are updated in many places, however, the most central place which directly corresponds to the user level Python code is the bytecode VM. If you write a simple Python function to add two variables, its bytecode looks as shown below.
>>> def add(a, b):
... return a b
...
>>> import dis
>>> dis.dis(add)
1 0 RESUME 0
2 2 LOAD_FAST 0 (a)
4 LOAD_FAST 1 (b)
6 BINARY_OP 0 ( )
10 RETURN_VALUE
Here, LOAD_FAST instruction is used to push the parameters a and b onto the stack and BINARY_OP is used to perform the operation on them. Let's take a look at how LOAD_FAST is implemented in CPython:
Let's break it down:
- GETLOCAL(oparg) looks up the object referred to by the parameter from the locals array.
- Next, the object's reference count is incremented by calling Py_INCREF because it is now being referred by the function parameter.
- STACK_GROW(1) grows the stack to hold the object
stack_pointer[-1] = value pushes the object onto the stack.
In summary, the LOAD_FAST instructions load the two objects passed to the function as parameters onto the stack and increments their reference counts. And, the BINARY_OP instruction, pops those objects off the stack and adds them. After the BINARY_OP instruction is done with the objects, it calls Py_DECREF to decrement their ref counts. At this point, if those objects are not referred by anything else, they will get freed.
This was a very curtailed tour of how ref counting is implemented in CPython. Note that, it does not work when objects have cyclic references between them. For such cases, CPython also uses a garbage collector.
Due to space and limitations on how many images I can put here,I have excluded many details and explanations. If you are interested in a deeper and full coverage, check out my article on this topic:
open.techwriters.info/codeco…