Python中对象的内存使用(一)
收藏


最近在了解Python语言中各种数据结构的使用的内存情况,写几篇文章和大家分享。

计算机存储单位

先铺垫一点基础知识。计算机存储单位一般用 Bit, Byte, KB, MB, GB, TB, PB等表示。他们由小到大递增:

  1. Bit(比特)。Bit是Binary digit(二进制数字)的缩写,最小的存储信息单位,存放一位二进制数,即 0 或 1。

  2. Byte(字节)。8个二进制位(Bit)为一个字节(B),字节是最常用的 存储容量单位。

  3. KB(Kilobyte,千字节)。1KB = 1024Byte

  4. MB(Megabyte,兆字节,简称「兆」)。1MB = 1024KB

  5. GB(Gigabyte,吉字节,又称「千兆」)。1GB = 1024MB

  6. TB(Terabyte,万亿字节,太字节)。1TB = 1024GB

  7. PB(Petabyte,千万亿字节,拍字节)。1PB = 1024TB

当然还有更大级别的单位,不常用就不说了。

获得Python对象占用的内存方法

在Python中 一切皆为对象,就不是象C语言中int占用4个字节这么简单了,Python提供了 sys.getsizeof获取对象所占用的字节大小。它支持任何类型的对象(本文例子都运行在Python 3.8下):

  1. venv/bin/ipython

  2. Python 3.8.0b3+ (heads/3.8:9bedb8c9e6, Aug 13 2019, 10:49:01)

  3. Type 'copyright', 'credits' or 'license' for more information

  4. IPython 7.7.0 -- An enhanced Interactive Python. Type '?' for help.


  5. In : import sys


  6. In : sys.getsizeof('a')

  7. Out: 50


  8. In : sys.getsizeof(1)

  9. Out: 28


  10. In : a = 1


  11. In : a.__sizeof__()

  12. 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):

  1. struct _longobject {

  2. PyObject_VAR_HEAD

  3. digit ob_digit[1];

  4. };

ob_digit是一个数组指针, digitint的别名。简单说一下Python整型的存储机制, ob_digit中的每个元素最大存储15 - 30位的二进制数(不同位数操作系统位数不同: 32位系统存15位,64位系统是30位)。假如在64位系统中,一个整数小于1073741824(2的30次方),它可以独立的放在 ob_digit的低位(索引为0),如果再大就把放不下的那部分放在索引为1的元素上,以此类推。做加减操作就是从低位起,在相对应的位作加减,并将多余的进位或不足的补位。

PyObject_HEAD是声明表示没有变化长度的对象的新类型时使用的宏(Include/object.h,延伸阅读链接3):

  1. #define PyObject_VAR_HEAD PyVarObject ob_base;

结构体 PyVarObject是这样的(Include/object.h,延伸阅读链接4):

  1. typedef struct {

  2. PyObject ob_base;

  3. Py_ssize_t ob_size;

  4. } PyVarObject;

其中 ob_size包含了整数正负符号信息和 ob_digit对象元素个数。结构体PyObject是这样的(Include/object.h,延伸阅读链接5):

  1. typedef struct _object {

  2. _PyObject_HEAD_EXTRA

  3. Py_ssize_t ob_refcnt;

  4. struct _typeobject *ob_type;

  5. } PyObject;

其中 _PyObject_HEAD_EXTRA以下划线开头的,这类变量一般都是内部使用,根据Include/object.h中的定义(延伸阅读链接6)可以知道只有在DEBUG模式下才有用,一般为空。

按阅读源码的顺序,逆向的看看28个字节内存在64位系统编译的Python中是这样分配的:

  1. _object.Py_ssize_t。8个字节用于引用计数器

  2. _object._typeobject。8个字节用于指向类型对象&PyLong_Type(类型为PyTypeObject *的指针)(延伸阅读链接7)。PyTypeObject具体的定义可以看延伸阅读链接8

  3. PyVarObject.Py_ssize_t。8个字节用于表示对象的可变长度部分中的字节数

  4. _longobject.digit。整数中每30位数字4个字节。我们上面的例子中整数1在这个范围,所谓只占4个字节。

作者是这么写的,但是过程很模糊,但我们需要确认一下。首先看 Py_ssize_t(configure文件中,延伸阅读链接8):

  1. #ifdef HAVE_SSIZE_T

  2. typedef ssize_t Py_ssize_t;

  3. #elif SIZEOF_VOID_P == SIZEOF_LONG

  4. typedef long Py_ssize_t;

  5. #else

  6. typedef int Py_ssize_t;

  7. #endif

对于我的Mac电脑来说,应该看Include/pymacconfig.h(延伸阅读链接9):

  1. ifdef __LP64__

  2. # define SIZEOF_LONG 8

  3. # define SIZEOF_VOID_P 8

在64位系统中,是C long类型的,64bits也就是8字节了。

另外是 _object._typeobject中引用的 ob_type这个指针变量所占内存大小取决于 ob_type的类型,可以看到 PyLong_Type有39位(Objects/longobject.c,延伸阅读链接10):

  1. PyTypeObject PyLong_Type = {

  2. PyVarObject_HEAD_INIT(&PyType_Type, 0)

  3. "int", /* tp_name */

  4. offsetof(PyLongObject, ob_digit), /* tp_basicsize */

  5. sizeof(digit),

  6. ....

PyLong_Type是int类型,但是由于位数超过4字节(32位),基于C语言数据结构补齐原则,需要补齐int的整数倍数位数,也就是64,就是8字节。找了半天没看到CPython的具体说明,但找到个辅证。在 Modules/_pickle.c里面序列化时 &PyLong_Type类型用的是Long类型保存的:

  1. ...

  2. else if (type == &PyLong_Type) {

  3. return save_long(self, obj);

  4. }

  5. ...

所以能确定这部分也是8字节。

PS: 上面这段是我的理解, 如果错误请指出!

那么整数1占用的内存就是: 8+8+8+4=28。再看看位宽超过30位的数字:

  1. In : sys.getsizeof((1 << 30) - 1)

  2. Out: 28


  3. In : sys.getsizeof((1 << 30))

  4. Out: 32


  5. In : sys.getsizeof((1 << 60))

  6. Out: 36


  7. In : sys.getsizeof((1 << 90))

  8. 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):

  1. static Py_ssize_t

  2. int___sizeof___impl(PyObject *self);


  3. static PyObject *

  4. int___sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored))

  5. {

  6. PyObject *return_value = NULL;

  7. Py_ssize_t _return_value;


  8. _return_value = int___sizeof___impl(self);

  9. if ((_return_value == -1) && PyErr_Occurred()) {

  10. goto exit;

  11. }

  12. return_value = PyLong_FromSsize_t(_return_value);


  13. exit:

  14. return return_value;

  15. }

也就是通过 int___sizeof___impl(self)获得对象占用字节数。接着找 int___sizeof___impl的实现(Objects/longobject.c,延伸阅读链接12):

  1. static Py_ssize_t

  2. int___sizeof___impl(PyObject *self)

  3. {

  4. Py_ssize_t res;


  5. res = offsetof(PyLongObject, ob_digit) + Py_ABS(Py_SIZE(self))*sizeof(digit);

  6. return res;

  7. }

Ok,到这里就找到终点了。我们反推一下,看看之前找的那个Stackoverflow上的回答对不对。

上面的实现中,offsetof是一个C语言的宏,找到结构成员相对于结构开头的字节偏移量。之前说int是 struct_longobject的实例,在这里也得到了印证:

  1. typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */

Py_ABS看名字可以猜出来: 返回数字的绝对值。Py_SIZE宏访问 selfob_sizesizeof是C语言中判断数据类型的函数,digit在CPython中这么定义(Include/longintrepr.h,延伸阅读链接13):

  1. #if PYLONG_BITS_IN_DIGIT == 30

  2. typedef uint32_t digit;

  3. ...

在64位系统中,C中sizeof(uint32_t)的结果是4。好,到这里就非常清晰了。整数占用28字节包含2部分:

  1. offsetof(PyLongObject,ob_digit)。这个偏移量就是前面我们看结构体的 _object.Py_ssize_t + _object._typeobject + PyVarObject.Py_ssize_t = 24。

  2. Py_ABS(Py_SIZE(self))*sizeof(digit)。其中 ob_size为1, sizeof(digit)为4,所以整体的结果是4。

后记

我认为学习就要举一反三,不是看人家的答案认为是这样的,要带着辩证思维,小心求证,这样才能真的理解。

下一篇我们继续学习常见的Python内置数据结构和容易占用的空间,及其中的一些问题和思考~

延伸阅读

  1. Why does python implementation use 9 times more memory than C?

  2. https://github.com/python/cpython/blob/3.8/Include/longintrepr.h#L85-L88

  3. https://github.com/python/cpython/blob/3.8/Include/object.h#L96

  4. https://github.com/python/cpython/blob/3.8/Include/object.h#L113-L116

  5. https://github.com/python/cpython/blob/3.8/Include/object.h#L104-L108

  6. https://github.com/python/cpython/blob/3.8/Include/object.h#L67-L78

  7. https://docs.python.org/3.8/c-api/long.html#c.PyLong_Type

  8. https://github.com/python/cpython/blob/3.8/configure#L16437-L16443

  9. https://github.com/python/cpython/blob/3.8/Include/pymacconfig.h#L41-L45

  10. https://github.com/python/cpython/blob/3.8/Objects/longobject.c#L5726-L5767

  11. https://github.com/python/cpython/blob/3.8/Objects/clinic/longobject.c.h#L99-L116

  12. https://github.com/python/cpython/blob/3.8/Objects/longobject.c#L5387-L5401

  13. https://github.com/python/cpython/blob/3.8/Include/longintrepr.h#L45

  14. https://docs.python.org/3/c-api/typeobj.html

  15. https://www.hongweipeng.com/index.php/archives/1222/

官方公众号