You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

设置NDArray视图标志为何引发内存分配?是否存在内存泄漏?

为NDArray视图设置不可写标志引发的内存分配问题

问题背景

为什么给NDArray视图设置不可写标志会引发内存分配?这种分配是否处于可控范围?

看下面的代码:

import numpy as np
import itertools


def get_view(arr):
    view = arr.view()
    view.flags.writeable = False  # 这行代码会导致内存增长?
    return view


def main():
    for _ in itertools.count():
        get_view(np.zeros(1000))


if __name__ == "__main__":
    main()

运行这段代码时,设置视图为不可写的那行似乎引发了内存增长,但不确定这种增长是否可控。

需要解答两个问题:

  1. 为什么会出现这种情况?
  2. 这种内存增长是否能保证可控?这是NumPy的Bug吗?还是相关对象用了引用计数,但手动调用垃圾回收器也无法回收?

内存追踪测试代码

下面是添加了tracemalloc逻辑的测试程序,每调用10万次get_view就打印内存分配情况:

import numpy as np
import tracemalloc
import itertools
import gc


def log_diff(snapshot, prev_snapshot):
    diff = snapshot.compare_to(prev_snapshot, "lineno")
    reported = 0
    for stat in diff:
        if "tracemalloc.py" in stat.traceback[0].filename:
            continue
        if stat.size_diff <= 0:
            continue
        print(f"#{reported}: {stat}")
        reported += 1
    print("---")


def get_view(arr):
    view = arr.view()
    view.flags.writeable = False  # 这行代码会导致内存增长?
    return view


def main():
    tracemalloc.start()
    prev_snapshot = None
    for i in itertools.count():
        get_view(np.zeros(1000))
        if i % 100000 == 0:
            gc.collect(generation=2)
            snapshot = tracemalloc.take_snapshot()
            if prev_snapshot is not None:
                log_diff(snapshot, prev_snapshot)
            prev_snapshot = snapshot


if __name__ == "__main__":
    main()

测试环境与观察结果

在Linux系统、Python 3.11.6、NumPy 1.26.4环境下,内存分配的数量看起来不稳定,但观察到的最大增长约为250次,前期增长较快,后期增速大幅放缓。

如果注释掉设置view.flags.writeable的代码行,内存占用不会增长。

tracemalloc输出结果:

#0: /home/sami/bug.py:22: size=3534 B (+3477 B), count=62 (+61), average=57 B
#1: /home/sami/bug.py:29: size=84 B (+28 B), count=2 (+1), average=42 B
---
#0: /home/sami/bug.py:22: size=5871 B (+2337 B), count=103 (+41), average=57 B
#1: /home/sami/bug.py:15: size=72 B (+72 B), count=1 (+1), average=72 B
---
---
#0: /home/sami/bug.py:22: size=6270 B (+399 B), count=110 (+7), average=57 B
---
#0: /home/sami/bug.py:22: size=6327 B (+57 B), count=111 (+1), average=57 B
---
#0: /home/sami/bug.py:22: size=7638 B (+1311 B), count=134 (+23), average=57 B
---
#0: /home/sami/bug.py:22: size=7809 B (+171 B), count=137 (+3), average=57 B
---
---
#0: /home/sami/bug.py:22: size=8436 B (+627 B), count=148 (+11), average=57 B
---
#0: /home/sami/bug.py:22: size=8664 B (+228 B), count=152 (+4), average=57 B
---
#0: /home/sami/bug.py:22: size=8892 B (+228 B), count=156 (+4), average=57 B
---
---
#0: /home/sami/bug.py:22: size=9120 B (+228 B), count=160 (+4), average=57 B
---
---
#0: /home/sami/bug.py:22: size=9177 B (+114 B), count=161 (+2), average=57 B
---
...

问题解答

1. 为什么会出现内存增长?

NumPy的数组视图默认和原数组共享底层的数据缓冲区。当你尝试将视图的writeable标志设为False时,NumPy不能直接修改原数组的可写属性——否则会影响其他共享该缓冲区的视图或原数组。因此,NumPy会创建一个新的只读数组包装对象,这个对象仍然共享原数据缓冲区,但拥有独立的标志位,标记为不可写。

在循环代码中,每次调用get_view都会生成这个只读包装对象,原数组会被该包装对象引用。虽然函数执行完后包装对象和原数组都应该被垃圾回收,但由于Python的分代GC机制,或是NumPy内部的临时缓存/对象复用逻辑,这些对象的回收会有延迟,导致短时间内内存出现增长。

2. 内存增长是否可控?这是Bug吗?

这种内存增长是可控的,不是NumPy的Bug。从tracemalloc的输出可以看到,内存增长到一定程度后就会大幅放缓,最终趋于稳定。原因如下:

  • Python的垃圾回收器最终会回收这些延迟处理的对象,尤其是手动调用gc.collect(generation=2)后,大部分未被回收的对象会被清理。
  • NumPy内部可能会复用一些只读视图的内部结构,避免重复创建大量相同的对象,从而限制内存增长的上限。

手动调用GC无法立即回收所有对象,是因为部分对象可能还处于年轻代,或是被临时的内部引用持有,但这些引用最终会被释放,对象也会被回收,不会出现无限的内存泄漏。


内容的提问来源于stack exchange,提问作者Sami Liedes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 09:55:53