Python中对象的内存使用(一)
计算机存储单位
先铺垫一点基础知识。计算机存储单位一般用 Bit, Byte, KB, MB, GB, TB, PB等表示。他们由小到大递增:
存储容量
当然还有更大级别的单位,不常用就不说了。
获得Python对象占用的内存方法
在Python中 一切皆为对象
,就不是象C语言中int占用4个字节这么简单了,Python提供了 sys.getsizeof
获取对象所占用的字节大小。它支持任何类型的对象(本文例子都运行在Python 3.8下):
❯ venv/bin/ipython Python 3.8.0b3+ (heads/3.8:9bedb8c9e6, Aug 13 2019, 10:49:01) Type 'copyright', 'credits' or 'license' for more information IPython 7.7.0 -- An enhanced Interactive Python. Type '?' for help. In : import sys In : sys.getsizeof('a') Out: 50 In : sys.getsizeof(1) Out: 28 In : a = 1 In : a.__sizeof__() Out: 28
可以看到除了用 sys.getsizeof
,还可以用对象的 __sizeof__()
方法。可以看到占用的空间远超C语言的实现: 这是因为Python对象的结构体更复杂,成员更多。
整数1的28个字节怎么分配的?
整数1占了28个字节,第一感觉肯定是好大啊!那这些内存空间是怎么分配的呢?我找到了一篇解释(见延伸阅读链接1),基于它的思路,这里用Python 3.8的C API来分析。
Python 3中int类型是长整型,所以int是 struct _longobject
的实例(Include/longintrepr.h,具体代码片段见延伸阅读链接2):
struct _longobject { PyObject_VAR_HEAD digit ob_digit[1]; };
ob_digit
是一个数组指针, digit
是 int
的别名。简单说一下Python整型的存储机制, ob_digit
中的每个元素最大存储15 – 30位的二进制数(不同位数操作系统位数不同: 32位系统存15位,64位系统是30位)。假如在64位系统中,一个整数小于1073741824(2的30次方),它可以独立的放在 ob_digit
的低位(索引为0),如果再大就把放不下的那部分放在索引为1的元素上,以此类推。做加减操作就是从低位起,在相对应的位作加减,并将多余的进位或不足的补位。
而 PyObject_HEAD
是声明表示没有变化长度的对象的新类型时使用的宏(Include/object.h,延伸阅读链接3):
#define PyObject_VAR_HEAD PyVarObject ob_base;
结构体 PyVarObject
是这样的(Include/object.h,延伸阅读链接4):
typedef struct { PyObject ob_base; Py_ssize_t ob_size; } PyVarObject;
其中 ob_size
包含了整数正负符号信息和 ob_digit
对象元素个数。结构体PyObject是这样的(Include/object.h,延伸阅读链接5):
typedef struct _object { _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
其中 _PyObject_HEAD_EXTRA
以下划线开头的,这类变量一般都是内部使用,根据Include/object.h中的定义(延伸阅读链接6)可以知道只有在DEBUG模式下才有用,一般为空。
按阅读源码的顺序,逆向的看看28个字节内存在64位系统编译的Python中是这样分配的:
_object.Py_ssize_t _object._typeobject PyVarObject.Py_ssize_t _longobject.digit
作者是这么写的,但是过程很模糊,但我们需要确认一下。首先看 Py_ssize_t
(configure文件中,延伸阅读链接8):
#ifdef HAVE_SSIZE_T typedef ssize_t Py_ssize_t; #elif SIZEOF_VOID_P == SIZEOF_LONG typedef long Py_ssize_t; #else typedef int Py_ssize_t; #endif
对于我的Mac电脑来说,应该看Include/pymacconfig.h(延伸阅读链接9):
ifdef __LP64__ # define SIZEOF_LONG 8 # define SIZEOF_VOID_P 8
在64位系统中,是C long类型的,64bits也就是8字节了。
另外是 _object._typeobject
中引用的 ob_type
这个指针变量所占内存大小取决于 ob_type
的类型,可以看到 PyLong_Type
有39位(Objects/longobject.c,延伸阅读链接10):
PyTypeObject PyLong_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "int", /* tp_name */ offsetof(PyLongObject, ob_digit), /* tp_basicsize */ sizeof(digit), ....
PyLong_Type
是int类型,但是由于位数超过4字节(32位),基于C语言数据结构补齐原则,需要补齐int的整数倍数位数,也就是64,就是8字节。找了半天没看到CPython的具体说明,但找到个辅证。在 Modules/_pickle.c
里面序列化时 &PyLong_Type
类型用的是Long类型保存的:
... else if (type == &PyLong_Type) { return save_long(self, obj); } ...
所以能确定这部分也是8字节。
PS: 上面这段是我的理解, 如果错误请指出!
那么整数1占用的内存就是: 8 + 8 + 8 + 4 = 28
。再看看位宽超过30位的数字:
In : sys.getsizeof((1 << 30) - 1) Out: 28 In : sys.getsizeof((1 << 30)) Out: 32 In : sys.getsizeof((1 << 60)) Out: 36 In : sys.getsizeof((1 << 90)) Out: 40
这样也能得出 每多30位宽,多占用4字节
。前面提到 _longobject
的结构体中 digit
指向 ob_digit[1]
而不是 ob_digit[0]
,也就是指向了高位,但事实上我们常用的都要小于30位,用不到 ob_digit[1]
,也就是0,这让我很困惑:没有看到整数存在了哪里?(欢迎留言解释下)
不完全理解,那就要学习CPython的源码。这次我们换个思路想问题,先看看 __sizeof__
方法的返回值是怎么来的(Objects/clinic/longobject.c.h,延伸阅读链接11):
static Py_ssize_t int___sizeof___impl(PyObject *self); static PyObject * int___sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyObject *return_value = NULL; Py_ssize_t _return_value; _return_value = int___sizeof___impl(self); if ((_return_value == -1) && PyErr_Occurred()) { goto exit; } return_value = PyLong_FromSsize_t(_return_value); exit: return return_value; }
也就是通过 int___sizeof___impl(self)
获得对象占用字节数。接着找 int___sizeof___impl
的实现(Objects/longobject.c,延伸阅读链接12):
static Py_ssize_t int___sizeof___impl(PyObject *self) { Py_ssize_t res; res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit); return res; }
Ok,到这里就找到终点了。我们反推一下,看看之前找的那个Stackoverflow上的回答对不对。
上面的实现中,offsetof是一个C语言的宏,找到结构成员相对于结构开头的字节偏移量。之前说int是 struct _longobject
的实例,在这里也得到了印证:
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
而 Py_ABS
看名字可以猜出来: 返回数字的绝对值。 Py_SIZE
宏访问 self
的 ob_size
, sizeof
是C语言中判断数据类型的函数,digit在CPython中这么定义(Include/longintrepr.h,延伸阅读链接13):
#if PYLONG_BITS_IN_DIGIT == 30 typedef uint32_t digit; ...
在64位系统中,C中sizeof(uint32_t)的结果是4。好,到这里就非常清晰了。整数占用28字节包含2部分:
-
offsetof(PyLongObject, ob_digit)
。这个偏移量就是前面我们看结构体的_object.Py_ssize_t
+_object._typeobject
+PyVarObject.Py_ssize_t
= 24。 -
Py_ABS(Py_SIZE(self))*sizeof(digit)
。其中ob_size
为1,sizeof(digit)
为4,所以整体的结果是4。
后记
我认为学习就要举一反三,不是看人家的答案认为是这样的,要带着辩证思维,小心求证,这样才能真的理解。
下一篇我们继续学习常见的Python内置数据结构和容易占用的空间,及其中的一些问题和思考~