读完文件并释放字符串后,tracemalloc 跟踪的当前分配量几乎回到起点,进程 RSS 却不一定回到读文件前。这里有两种不同的问题:Python 对象是否仍被引用,以及分配器和操作系统当前把多少内存记在进程名下。仅凭其中一个指标,不能推断另一个必须同步下降。

16 的文件作用域提供 open_word_file,本篇用同一份合成输入比较 source.read() 全量持有字符串与 open_word_file 按行词元化。文件是 30000 行 One two one、360000 字节,两边结果都为 {"one":60000,"two":30000},且退出作用域后 source.closed 均为真。每种模式在独立子进程测量,以免前一模式的 RSS 高水位污染后一模式;写入输入文件后才开启 tracemalloc,只测读取和统计路径。

三种内存数字不互相代替

当前 PyPy 8.0.0 归档没有 _tracemalloc,但提供 gc.get_stats(memory_pressure=True)。脚本在 PyPy 上分别保存处理前、处理中和 GC 后的 GC used、GC allocated、进程历史峰值、JIT 内存与外部分配压力,allocation_tracing 标为 pypy_gc_stats;普通和 free-threaded CPython 仍实际运行 tracemalloc,标为 cpython_tracemalloc。两组接口口径不同:PyPy 是解释器 GC 堆聚合快照,不提供 CPython tracemalloc 的逐分配调用栈;输出继续同时保存真实 RSS、词频、文件关闭和循环收集结果。三环境复跑见 writing-plans/python-language/completion/evidence/pypy-repair/after-*-memory.stdout。

历史 Ubuntu/CPython 3.12.3 环境曾留下如下观察(单位字节;ru_maxrss 在 Linux 下以 KiB 报告高水位,表格已按字节口径说明)。这组数字是旧证据,不能冒充本次 macOS 复跑:

模式 traced peak GC 后 traced 当前 RSS 读前 RSS GC 后
全量 725250 1467 16080896 16162816
流式 23198 1813 16084992 16084992

全量路径明确删除 data 后调用 gc.collect();历史 Linux 观测里 traced 当前分配约 1.5 KB,RSS 仍比读前高 81920 字节。这次观测没有证明有泄漏:引用还可能保留在 counts 或测试框架中,tracemalloc 不覆盖所有原生分配,进程 RSS 也受分配器缓存、内存页和测量时点影响。流式路径峰值更低,理由与持有整份文本字符串的范围一致;一次输入和两次独立运行不足以推断所有文件大小上的比例关系。36 的基准也不能拿这些值直接填补 wall time 或吞吐结论。

本次在 macOS/Python 3.14.4 上复跑 100 行输入时,脚本没有再读取 Linux 专属的 /proc/self/statm。当前 RSS 由 Mach task_info 的 resident_size 给出,resource.getrusage().ru_maxrss 在 macOS 下按字节记录历史峰值,两者在输出里分别标为 current_rss_*_bytes 与 peak_rss。这次样本中全量路径的 current_rss_during_bytes 为 23691264,流式路径为 23527424;两条 peak_rss.source 都是 ru_maxrss_bytes。这些数值只说明本机本输入的读数口径已经区分当前 RSS 与峰值 RSS,不说明 macOS 与旧 Ubuntu 的数字可以横向比较。

实验另造一个仅由自身引用保持的循环对象:删除外部引用后、显式 GC 前 weakref 仍能取到对象,GC 后变成 None。这条断言证明此例循环对象的可达性发生变化,不表示所有对象都需要循环收集,更不表示 GC 后 RSS 必然下降。文件是否关闭仍由 with 作用域证明,不能以垃圾回收或进程最终退出替代。

1
2
3
4
cd examples/python-language
PYTHON=${PYTHON:-python3}
PYTHONPATH=src "$PYTHON" benchmarks/37_memory.py --lines 30000
PYTHONPATH=src "$PYTHON" -m unittest tests.test_memory_measure -v

旧样本的解释器、页大小、输入长度、两进程原始指标、输出断言和累计 95 项测试在 examples/python-language/evidence/37/20261008-memory/。本次 macOS 复跑记录在 examples/python-language/evidence/completion/core/final-37_memory.log。Linux 当前 RSS 来源是 /proc/self/statm;macOS 当前 RSS 来源是 Mach task_info;resource.ru_maxrss 只作为峰值 RSS 记录,且单位随平台不同。其他平台接口未经实验,不引用本表泛化。

预测题

删去全量模式持有的 data,gc.collect() 后 traced 当前分配很低,能否推断 RSS 必定回到初始值?若测试提前退出流式迭代,谁关闭源文件?

答案:不能推断 RSS 必降;它记录进程页而非 tracemalloc 当前跟踪对象的总和,缓存和分配器可能继续保留页。提前离开 with open_word_file(...) 时由文件作用域关闭 source,并在 finally 关闭词元生成器,不能等待 GC 替它履约。

变更题

把全量模式跑完的同一进程拿来测流式模式,只比较 ru_maxrss,若数值没有下降就断言“流式同样占用内存”,这个结论哪里错?

答案:ru_maxrss 是历史峰值,同进程后一个模式不会让历史峰值回落。应独立运行并记录当前 RSS、输入、时间范围与分配追踪口径;两次进程启动的差异也须列为噪声,不能把单次差值认作普遍内存节省比例。

系列导航只列已验收文章。

参考资料