12 Comments
Aug 29, 2023·edited Aug 29, 2023Liked by Abhinav Upadhyay

Thank you for this amazing article. Although it was a bit beyond my scope to understand CPython's internal code, it was still an amazing read about reference counting. I am looking forward to an article about the Garbage Collector.

Expand full comment
author

Thank you, Sanjit! I am sure in some time, the parts which did not make sense will seem natural to you. I remember, few years back trying to understand how Python works and not able to understand anything. But, now I can read it as if it was my personal project :)

Expand full comment

I’m definitely looking forward to the GC companion to this article...

Expand full comment
author

Thank you! Yeah, some interesting stuff hidden in GC implementation details as well.

Expand full comment

I actually did have a question. Admittedly, I'll need to pour over the details of the CPython code you show in more detail as the answer may be there, but if one writes:

`SomeInstance()`

or

`"hello"`

i.e. create an instance without assigning it to a variable name, what does the ref counter do? Let's assume this is in a script rather than the REPL for this argument, as the Python standard REPL does create a reference in this case to `_`

Expand full comment
author

That's an interesting question. I think someone else asked a similar question on Reddit as well, but I got banned before I could answer. Let me give it a try. I am not sure how well the formatting is going to look like in comments here.

Consider the following code for reference:

>>> class Foo():

... def __init__(self):

... self.x = 10

...

>>> def test_foo():

... Foo()

I think test_foo() describes the scenario you posed. The function creates a new Foo but doesn't assign it to anything. Let's look at its bytecode.

>>> dis.dis(test_foo)

1 0 RESUME 0

2 2 LOAD_GLOBAL 1 (NULL + Foo)

12 CALL 0

20 POP_TOP

22 RETURN_CONST 0 (None)

The CALL instruction is going to call the constructor of Foo, which underneath is going to create a new class object. When the object is created, its ref count is set to 1. As the constructor function returns an instance of the object, that object is pushed to the top of the stack when the CALL instruction is finished. The POP_TOP instruction simply pops the top of the stack and decrements its ref count. Which means that the object's ref count would reach 0 and it would get deallocated before test_foo() returns.

Let's see a different example where we do assign the instance to a variable.

>>> def test_foo2():

... f = Foo()

...

>>> dis.dis(test_foo2)

1 0 RESUME 0

2 2 LOAD_GLOBAL 1 (NULL + Foo)

12 CALL 0

20 STORE_FAST 0 (f)

22 RETURN_CONST 0 (None)

It's a similar bytecode but the POP_TOP got replaced by STORE_FAST. The STORE_FAST instruction takes the pops the top of the stack and stores it in the locals array. Each function has a locals array which holds objects local to it. STORE_FAST does not modify the ref count of the object, so it still remains at 1. However, after RETURN_CONST as the interpreter will switch control to the caller, it will clean up the stackframe for the test_foo2() function and as part of that it will clean up the locals of that function, which means decrementing ref counts of all the objects in it. So again, that object will get deallocated.

A final scenario:

>>> def test_foo3():

... return Foo()

...

>>> dis.dis(test_foo3)

1 0 RESUME 0

2 2 LOAD_GLOBAL 1 (NULL + Foo)

12 CALL 0

20 RETURN_VALUE

Here, the test_foo3 function calls the Foo constructor and returns it. In this case after the CALL instruction, the object is at the top of the stack with ref count 1. (it is not pushed to locals this time). The RETURN_VALUE instruction takes the current stack top and makes it the return value without changing the ref count.

Expand full comment
author

In case, you try to create an object within a script without assigning to the variable, that amounts to the same case as I showed in test_foo(). The callable gets called, becomes the top of the stack and POP_TOP pops it, decrements the ref count.

Expand full comment

Thanks for the detailed response

Expand full comment

So effectively, the ref count is 1 for the briefest of moments…

Expand full comment
author

Yes, exactly.

Expand full comment

> In the case of CPython, it employs two techniques for managing memory: reference counting and garbage collection.

I'd say that CPython's primary method for memory management is through manual 'malloc' and 'alloc' functions. Its secondary method is garbage collection. Reference counting is only useful for garbage collection.

Expand full comment

This is great, please write more! )

Expand full comment