记一次列表预分配空间的锅
花下猫语:Python 中的列表是可变对象,但是在每次扩容的时候,并不是要加入多少新元素,就申请多少新的内存空间,而是采用了超额分配的机制,在所需空间之外,还会多分配一些空间。
我之前的文章《Python对象的空间边界:独善其身与开放包容》介绍过这个特性,今天再分享一篇文章,对此问题做了更详细的专题介绍。
作者:weapon
原文:https://zhuanlan.zhihu.com/p/47006584
起步
两个存储元素内容相同的列表占用内存空间可能会不一样。
甚至 a == b
都是成立的。是什么导致了占用的空间不一致的呢?
列表对象的存储方式
Python 中 list 的实现方式和 C++ 的 vector
类似,它并不是存多少东西就申请多少内存,它会申请一块较大的内存,避免每次新增元素都要进行内存申请和元素拷贝。当空间不足以容纳新元素时会进行扩容。
上图中 [0]
时是确定的元素个数,就只申请容纳一个元素空间,而 append
追加的方式会导致 list 对象扩容。
解答
为了解释最开始图片里的问题,还要先知道乘法 *
和 *=
是两个不同的操作符。这点上可以通过字节码来得到:
import dis
def fun():
a = [0]
a = a * 10
b = [0]
b *= 10
dis.dis(fun)
相关的字节码为:
6 LOAD_FAST 0 (a)
8 LOAD_CONST 2 (10)
10 BINARY_MULTIPLY # * 操作符
12 STORE_FAST 0 (a)
...
20 LOAD_FAST 1 (b)
22 LOAD_CONST 2 (10)
24 INPLACE_MULTIPLY # *= 操作符
26 STORE_FAST 1 (b)
我们初学时总是会将 a *= n
理解为 a = a * n
(当然这里理解是没错的),深入后就会发现他们的不同了。用 Python 中的魔术方法来说就是一个会调用 __mul__
一个会调用 __imul__
。
而 list 对象是 C 实现的,自然是调用 C 函数了,乘法操作会调用 list_repeat()
;*=
会调用list_inplace_repeat()
。
[listobject.c v3.6.5]
static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
...
size = Py_SIZE(a) * n;
if (size == 0)
return PyList_New(0);
np = (PyListObject *) PyList_New(size); // 创建容纳size个空间的列表
...
}
从这可以看出 list_repeat
需要多少空间就申请多少空间,从这里也可以看出乘法操作是返回一个新的列表对象。
再来看看 list_inplace_repeat
:
[listobject.c v3.6.5]
static PyObject *
list_inplace_repeat(PyListObject *self, Py_ssize_t n)
{
...
size = PyList_GET_SIZE(self);
...
if (list_resize(self, size*n) < 0)
return NULL;
...
}
代码中试图通过 list_resize
来进行扩容,并告诉它这个列表需要容纳 size * n
个元素,那么 resize 函数里面会申请比所需的空间还要大点的内存吗?显然是的:
[listobject.c v3.6.5]
static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
...
new_allocated = newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
...
}
resize 后的空间 总是 比所需要的大的。按照这里的扩容规则,如果一个空列表通过 append
不断往里面添加元素,那么空间占用会是 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
所以这个 *=
会引起列表 resize,而比 *
的方式占用空间大;解释完毕。
延伸
为了验证空间增长规律,看看下面的例子:
这里只要说变量 e 就可以了,它不同其实是因为它不是从空列表增长上来的。它的初始大小是 1。那么知道 resize 规则,应该能列出变量 e 的增长顺序了吧。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 详解Linux如何生成随机数字和字符串
- linux(ubuntu)用户连续N次输入错误密码进行登陆时自动锁定X分钟
- linux下安装golang的方法
- Linux系统的文件传输方法
- CentOS 6.8 NFS 文件共享设置的方法
- linux如何mount挂载磁盘并设置开机自动mount的实现
- 浅谈Linux的编码及编码转换方法
- 在 Linux 上用 DNS 实现简单的负载均衡的方法
- centos7.2.1511安装jdk1.8.0_151及mysql5.6.38的方法
- Linux CentOS使用crontab设置定时重启的方法
- centos安装php5、卸载php、安装php7的教程
- centos7中crontab定时计划任务5分钟一次命令写法
- Ubuntu16.04 安装Teamviewer的教程详解
- 01 . OpenResty简介部署,优缺点,压测,适用场景及用Lua实现服务灰度发布
- 详解Linux iptables 命令