python局部变量内存地址释放疑问的分析

Posted by Shi Hai's Blog on June 2, 2022

一、问题背景

遇到了一段代码,如下所示,第一眼的直觉是变量ins是一个局部变量,重复调用test()函数会反复创建新的实例ins

import sys


def test():
    ins = object()
    print(id(ins))


while True:
    test()

但实际情况却是在进程运行期间局部变量insid一直没有发生改变。

140076922921824
140076922921824
140076922921824
140076922921824

二、原因分析

2.1 分析引用计数

在上面示例代码中补充一行代码看一下实际的ins引用变量计数情况,代码如下所示:

mport sys


def test():
    ins = object()
    print(id(a))
    ref = sys.getrefcount(ins)
    print(f'引用计数: {ref}')


while True:
    test()

执行此示例代码的输出结果是:

139765934961536
引用计数: 2
139765934961536
引用计数: 2
139765934961536
引用计数: 2

这里读者可能会产生一个疑问:这里只有变量ins引用到object()实例,为什么引用计数是2?
这个结果是因为调用sys.getrefcount()函数的时候会多产生一个引用计数,具体gc的管理机制可以点这里
从上面引用计数输出看,实际唯一的一个引用计数就是ins变量,那我们删除一下此变量引用后再看object()实例id是否发生变化?

import sys


def test():
    ins = object() 
    print(id(ins))
    del ins


while True:
    test()

执行上述代码看ins的实例ins的id还是没有发生任何变化,实际输出如下所示。
这里和我判断的预期情况不一样,我的预期情况是ins唯一引用被删除后就会触发GC行为,ins对应的内存块就会被回收。

140306047644512
140306047644512
140306047644512

进一步修改示例代码查看实际的实例删除动作是否进行了?如下所示。

import sys
import gc

class Son(object):

    def __del__(self):
        print('Deleting instance')


def test():
    ins = Son()
    print(id(ins))


while True:
    test()

执行上述代码后的输出结果如下,从结果上看可以确认__del__确实是执行了,那这样还有一种推测就是每一次创建ins实例都是在原有内存中创建出来而导致实例id未发生改变。

140221875591104
Deleting instance
140221875591104
Deleting instance
140221875591104
Deleting instance

实际查看id()的帮助文档发现,其实没有时间重合度的对象有可能分配同个内存地址而导致id()值一致。我翻看了CPython的解释过程(参见2.2)和内存创建环节可以确认是简单调用malloc()函数进行内存创建活动。

2.2 分析字节码

我们换个思路重新审视一下:从字节码入手看是否能看出一些蛛丝马迹或者排除一些无关项。
使用python3 -m dis test.py可以输出python代码对应的字节码信息:

  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)

  4           8 LOAD_CONST               2 (<code object test at 0x7fece3319a80, file "test.py", line 4>)
             10 LOAD_CONST               3 ('test')
             12 MAKE_FUNCTION            0
             14 STORE_NAME               1 (test)

 10     >>   16 LOAD_NAME                1 (test)
             18 CALL_FUNCTION            0
             20 POP_TOP
             22 JUMP_ABSOLUTE           16

Disassembly of <code object test at 0x7fece3319a80, file "test.py", line 4>:
  5           0 LOAD_GLOBAL              0 (object)
              2 CALL_FUNCTION            0
              4 STORE_FAST               0 (ins)

  6           6 LOAD_GLOBAL              1 (print)
              8 LOAD_GLOBAL              2 (id)
             10 LOAD_FAST                0 (ins)
             12 CALL_FUNCTION            1
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

从上面输出的字节码来看,和ins变量相关的只有两处指令操作:

  • CALL_FUNCTION: 调用object()创建出相关的实例;
  • STORE_FASTLOAD_FAST: 对ins变量进行存取操作。 排查这三个指令对应cpython代码可以确认都是正常的指令调用没有涉及GC相关活动。

2.2.1 CALL_FUNCTION

CPython 编译器内的指令代码如下所示,实际这个指令的动作就是调用C层面的函数执行,无内存管理相关动作。

        case TARGET(CALL_FUNCTION): {                                               
            PREDICTED(CALL_FUNCTION);                                               
            PyObject **sp, *res;                                                    
            sp = stack_pointer;                                                     
            res = call_function(tstate, &trace_info, &sp, oparg, NULL);             
            stack_pointer = sp;                                                     
            PUSH(res);                                                              
            if (res == NULL) {                                                      
                goto error;                                                         
            }                                                                       
            CHECK_EVAL_BREAKER();                                                   
            DISPATCH();                                                             
        }                                      

2.2.2 STORE_FAST/LOAD_FAST

CPython 编译器内的指令代码如下所示,实际这两个指令的动作就是在fastlocals数组中存取相关变量。

        case TARGET(STORE_FAST): {                                              
            PREDICTED(STORE_FAST);                                              
            PyObject *value = POP();                                            
            SETLOCAL(oparg, value);                                             
            DISPATCH();                                                         
        }           

        case TARGET(LOAD_FAST): {                                               
            PyObject *value = GETLOCAL(oparg);                                  
            if (value == NULL) {                                                
                format_exc_check_arg(tstate, PyExc_UnboundLocalError,           
                                     UNBOUNDLOCAL_ERROR_MSG,                    
                                     PyTuple_GetItem(co->co_varnames, oparg));  
                goto error;                                                     
            }                                                                   
            Py_INCREF(value);                                                   
            PUSH(value);                                                        
            DISPATCH();                                                         
        }                                                    

参考附录

Puzzled with LOAD_FAST/STORE_FAST of python
How can two Python objects have same id but 'is' operator returns False?