Scipy稀疏矩阵内存占用异常问题咨询
我原本认为scipy的稀疏矩阵会比原生列表列表表示占用少得多内存,但实验结果却与预期不符。以下是简化的IPython会话:我构建了一个约75%元素为0的随机二进制矩阵,对比scipy.sparse中所有可用表示的内存占用情况:
# %load_ext memory_profiler, from scipy import sparse, etc. # ... %memit M = random_binary_matrix(5000, 5000) # contains ints peak memory: 250.36 MiB, increment: 191.77 MiB In : sum(line.count(0) for line in M) / (len(M) * len(M[0])) Out: 0.75004468 %memit X_1 = sparse.bsr_matrix(M) peak memory: 640.49 MiB, increment: 353.76 MiB %memit X_2 = sparse.coo_matrix(M) peak memory: 640.71 MiB, increment: 286.09 MiB %memit X_3 = sparse.csc_matrix(M) peak memory: 807.51 MiB, increment: 357.53 MiB %memit X_4 = sparse.csr_matrix(M) peak memory: 840.04 MiB, increment: 270.91 MiB %memit X_5 = sparse.dia_matrix(M) peak memory: 1075.20 MiB, increment: 386.87 MiB %memit X_6 = sparse.dok_matrix(M) peak memory: 3059.86 MiB, increment: 1990.62 MiB %memit X_7 = sparse.lil_matrix(M) peak memory: 2774.67 MiB, increment: 385.39 MiB
我是否操作有误?或是忽略了什么(包括这些替代表示的核心意义)?还是memory_profiler工具本身的问题,或是我对其理解有误?特别是“峰值内存”与“增量内存”的关系有时看起来不合理:初始化X_2时内存增量为286.09 MiB,但峰值内存仅略高于执行前的水平。
环境信息:Debian 12、Python 3.11.2、IPython 8.5.0、scipy 1.10.1、memory_profiler 0.61.0
1. 稀疏矩阵的内存优势阈值误区
75%的零元素并不算“足够稀疏”——稀疏矩阵的内存优势通常在90%以上零元素的场景才会显现。你的5000×5000矩阵有1250万非零元素,原生列表列表的内存计算中,每个Python int在64位系统中占28字节(大规模矩阵中会有大量非池化整数对象),总内存接近测试中191MiB的增量。
而Scipy稀疏矩阵本身存在结构开销:比如CSR/CSC需要存储三个独立数组(数据、索引、指针),每个数组元素是8字节的数值类型,仅1250万非零元素的数据数组就占约95MiB,加上索引和指针数组,总内存轻松超过原生结构。只有当非零元素占比降到5%以下时,稀疏矩阵的内存优势才会凸显。
另外,你直接从原生列表列表构造稀疏矩阵的过程中,会先加载完整的原生矩阵,构造时还会生成临时数组,导致memory_profiler记录的峰值内存包含这些临时数据,并非稀疏矩阵最终稳定的内存占用。
2. memory_profiler指标的正确理解
- 增量内存:指执行代码前后的内存差值,代表该操作实际新增的内存(即稀疏矩阵最终留存的内存)。
- 峰值内存:执行过程中达到的最高内存,包含构造时的临时数据、尚未被GC回收的内存等。
比如X_2的增量286MiB是coo_matrix最终占用的内存,峰值仅略高于之前水平,是因为之前构造其他稀疏矩阵的临时内存已被GC回收,或系统内存有剩余,未触发更高峰值,这属于正常现象,并非工具问题。
3. 不同稀疏矩阵类型的内存特性
- DOK/LIL:这类结构为方便修改设计,用字典或列表存储非零元素,内存开销极大(DOK依赖哈希表,每个非零元素存键值对),完全不适合大规模静态矩阵,测试结果也印证了这一点。
- COO:存储非零元素的坐标和值,结构简单,内存开销相对小,但不支持直接运算。
- CSR/CSC:适合快速行/列访问和矩阵运算,内存开销比COO略高,但运算效率更优。
- DIA:仅适配对角线集中非零元素的矩阵,你的随机矩阵完全不匹配,因此内存开销最大。
4. 准确测试稀疏内存的方法
- 构造完成后手动触发GC(
import gc; gc.collect()),再测量内存。 - 结合
sys.getsizeof()和稀疏矩阵内部属性计算实际内存(比如CSC矩阵用data.nbytes + indices.nbytes + indptr.nbytes),而非仅依赖memory_profiler的增量值。 - 避免从原生列表列表构造,直接用
sparse.random等方法生成稀疏矩阵,跳过原生矩阵的内存占用干扰。
内容的提问来源于stack exchange,提问作者Anthony Labarre

