Python中对象的内存使用(一)
/ / / 阅读数:11894最近在了解 Python 语言中各种数据结构的使用的内存情况,写几篇文章和大家分享。
计算机存储单位
先铺垫一点基础知识。计算机存储单位一般用 Bit, Byte, KB, MB, GB, TB, PB 等表示。他们由小到大递增:
- Bit (比特)。Bit 是 Binary digit(二进制数字)的缩写,最小的存储信息单位,存放一位二进制数,即 0 或 1。
- Byte (字节)。8 个二进制位 (Bit) 为一个字节 (B),字节是最常用的
存储容量
单位。 - KB (Kilobyte)。1KB = 1024Byte
- MB (Megabyte,简称「兆」)。1MB = 1024KB
- GB (Gibibyte)。1GB = 1024MB
- TB (Terabyte)。1TB = 1024GB
- PB (Petabyte)。1PB = 1024TB
当然还有更大级别的单位,不常用就不说了。
获得 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): ```python 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): ```python #define PyObject_VAR_HEAD PyVarObject ob_base; ```结构体`PyVarObject`是这样的 (Include/object.h,延伸阅读链接 4):```python typedef struct { PyObject ob_base; Py_ssize_t ob_size; } PyVarObject; ```其中`ob_size`包含了整数正负符号信息和`ob_digit`对象元素个数。结构体 PyObject 是这样的 (Include/object.h,延伸阅读链接 5):```python 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 中是这样分配的: 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): ```python #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):```python 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 类型保存的: ```python ... 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): ```python 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):```python 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):```python #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 内置数据结构和容易占用的空间,及其中的一些问题和思考~### 延伸阅读
- Why does python implementation use 9 times more memory than C?
- https://github.com/python/cpython/blob/3.8/Include/longintrepr.h#L85-L88
- https://github.com/python/cpython/blob/3.8/Include/object.h#L96
- https://github.com/python/cpython/blob/3.8/Include/object.h#L113-L116
- https://github.com/python/cpython/blob/3.8/Include/object.h#L104-L108
- https://github.com/python/cpython/blob/3.8/Include/object.h#L67-L78
- https://docs.python.org/3.8/c-api/long.html#c.PyLong_Type
- https://github.com/python/cpython/blob/3.8/configure#L16437-L16443
- https://github.com/python/cpython/blob/3.8/Include/pymacconfig.h#L41-L45
- https://github.com/python/cpython/blob/3.8/Objects/longobject.c#L5726-L5767
- https://github.com/python/cpython/blob/3.8/Objects/clinic/longobject.c.h#L99-L116
- https://github.com/python/cpython/blob/3.8/Objects/longobject.c#L5387-L5401
- https://github.com/python/cpython/blob/3.8/Include/longintrepr.h#L45
- https://docs.python.org/3/c-api/typeobj.html
- https://www.hongweipeng.com/index.php/archives/1222/
@zdyxry 感谢~ 原来 Gigabyte 和 Gibibyte 不是一个东西 TIL。已改