用预留容量和缩容阈值避免列表反复搬迁
CPython 的 list 把对象指针连续存储,在扩容时适当预留空间,并在占用低于一半前避免缩容。
工程问题#
若每次 append 和 pop 都精确调整底层空间,长度在边界附近变化时就会反复调用分配器,长序列追加也可能不断搬迁已有指针。
实际实现#
list_resize 在新长度介于当前容量的一半与容量之间时只改逻辑长度。需要重分配时,它按 newsize 加约八分之一和常数余量计算容量并对齐;批量增长过大时减少额外预留,零长度特殊处理。
处理流程#
- 比较新长度与当前已分配容量
- 仍在半容量到满容量之间时,保留底层存储,只改长度
- 否则计算带余量且按四个指针槽对齐的新容量
- 对批量增长和空列表作调整,重分配后更新指针、长度与容量
源码片段#
固定版本 · 原文摘录51 /* Bypass realloc() when a previous overallocation is large enough52 to accommodate the newsize. If the newsize falls lower than half53 the allocated size, then proceed with the realloc() to shrink the list.54 */55 if (allocated >= newsize && newsize >= (allocated >> 1)) {56 assert(self->ob_item != NULL || newsize == 0);57 Py_SET_SIZE(self, newsize);58 return 0;59 }容量足够且新长度不低于容量一半时,直接更新长度并返回;删除一个元素并不必然缩容。
71 new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;72 /* Do not overallocate if the new size is closer to overallocated size73 * than to the old size.74 */75 if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))76 new_allocated = ((size_t)newsize + 3) & ~(size_t)3;7778 if (newsize == 0)79 new_allocated = 0;80 if (new_allocated <= (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {81 num_allocated_bytes = new_allocated * sizeof(PyObject *);82 items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);展示预留容量公式、批量增长抑制、空列表处理和重分配入口。后续代码还会检查分配失败,再更新对象字段。
两段位于同一函数,分别展示避免重分配的区间和真正重分配时的容量策略;完整错误处理保留在源文件中。
收益与取舍#
工程分析带来的收益
- 单元素连续追加通过预留容量分摊搬迁成本
- 扩容与缩容使用不同触发区间,减少长度小幅波动造成的反复重分配
- 批量增长时避免无益地保留过多额外槽位
付出的代价
- 列表容量可能显著大于当前长度,空间换取较少分配工作
- 一次扩容仍可能复制 O(n) 个指针;摊还结论不保证每次操作低延迟
- 连续的是 PyObject 指针数组,并非 Python 对象本体
可比较的选择
每次按长度精确分配闲置空间较少,但频繁追加或边界波动更容易反复重分配。
分块序列或双端队列可减少整体搬迁,但随机访问、局部性和空间布局有不同成本。
版本与适用范围#
对应 CPython v3.12.0 的内部 list_resize,不是 Python 语言规范保证的扩容比例;其他 Python 实现或版本可以采用不同策略。整体复杂度还受引用计数、对象析构和分配器行为影响。
参考来源#
源码、官方文档与原始论文相关实现与比较#
4 个相关案例nginx 解析 upstream 块时,用 ngx_array_t 连续保存服务器条目;每遇到一条 server 指令便追加一个元素。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。
Git 把内存中的 index 条目指针保存在有序、可增长的数组中,按路径和合并 stage 二分查找位置。