NumPy使用不当引起的内存泄漏

这篇具有很好参考价值的文章主要介绍了NumPy使用不当引起的内存泄漏。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

以下是一段会引起内存逐步累积的代码:

代码大意:处理n个user的数据,将每个user的数据按照时间排序,取最早的10条的前三列保存

import time
import tracemalloc
import numpy as np

result = []  # 保存结果
n, m = 5, 10000
for i in range(n):
    # 每个user造数据m条
    data = [[f'user{i}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)]
    new_data = np.array(sorted(data, key=lambda x: x[2]))
    result.append(new_data[:10, :3])

为了监控性能,我们使用python自带的分析工具tracemalloc跟踪内存分配,使用方法见:

Python-tracemalloc-跟踪内存分配_Rnan-prince的博客-CSDN博客

import time
import tracemalloc
import numpy as np

result = []  # 保存结果
n, m = 5, 10000
tracemalloc.start()
# 处理n个user的数据
for i in range(n):
    # 每个user造数据m条
    data = [[f'user{i}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)]
    new_data = np.array(sorted(data, key=lambda x: x[2]))
    result.append(new_data[:10, :3])

    snapshot = tracemalloc.take_snapshot()  # 内存摄像
    top_stats = snapshot.statistics('lineno')  # 内存占用数据获取
    print(f'[Handle user{i} Top 3]')
    for stat in top_stats[:3]:  # 打印占用内存最大的10个子进程
        print(stat)

运行结果:

[Handle user0 Top 3]
D:/MyPython/memory/demo.py:12: size=3047 KiB, count=4, average=762 KiB
D:/MyPython/memory/demo.py:11: size=4368 B, count=78, average=56 B
D:/MyPython/memory/demo.py:13: size=168 B, count=3, average=56 B
[Handle user1 Top 3]
D:/MyPython/memory/demo.py:12: size=6094 KiB, count=8, average=762 KiB
D:/MyPython/memory/demo.py:11: size=5480 B, count=79, average=69 B
D:\Program Files\anaconda3\lib\tracemalloc.py:209: size=848 B, count=2, average=424 B
[Handle user2 Top 3]
D:/MyPython/memory/demo.py:12: size=9141 KiB, count=12, average=762 KiB
D:/MyPython/memory/demo.py:11: size=5480 B, count=79, average=69 B
D:\Program Files\anaconda3\lib\tracemalloc.py:479: size=1776 B, count=25, average=71 B
[Handle user3 Top 3]
D:/MyPython/memory/demo.py:12: size=11.9 MiB, count=17, average=717 KiB
D:/MyPython/memory/demo.py:11: size=5480 B, count=79, average=69 B
D:\Program Files\anaconda3\lib\tracemalloc.py:479: size=1720 B, count=24, average=72 B
[Handle user4 Top 3]
D:/MyPython/memory/demo.py:12: size=14.9 MiB, count=20, average=762 KiB
D:/MyPython/memory/demo.py:11: size=5480 B, count=79, average=69 B
D:\Program Files\anaconda3\lib\tracemalloc.py:479: size=1776 B, count=25, average=71 B

运行这段代码,我们看到11行和12行的内存几乎呈线性不断增长,当n值足够大时,就会使程序崩溃,甚至操作系统会重新启动。

分析代码:每次循环处理user的10000条数据,将每个user的数据按照时间排序,取最早的10条的前三列形成小数组不断加入结果result。按理说,最后只应占有一份大数组data的内存和一份小数组new_data内存,小数组占用内存太小,不可能使内存以如此明显的速度增长。这说明每次大数组被覆盖后,原来的值所占用的内存都没有释放,随着迭代的进行,内存将以大数组的量级累加。

为了解决这个问题,我们尝试如下办法:

1、先尝试删除大数组data

import time
import tracemalloc
import numpy as np

result = []  # 保存结果
n, m = 5, 10000
tracemalloc.start()
# 处理n个user的数据
for i in range(n):
    # 每个user造数据m条
    data = [[f'user{i}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)]
    new_data = np.array(sorted(data, key=lambda x: x[2]))[:10, :3]
    result.append(new_data)
    del data

    snapshot = tracemalloc.take_snapshot()  # 内存摄像
    top_stats = snapshot.statistics('lineno')  # 内存占用数据获取
    print(f'[Handle user{i} Top 3]')
    for stat in top_stats[:3]:  # 打印占用内存最大的10个子进程
        print(stat)

2、data变量名覆盖

import time
import tracemalloc
import numpy as np

result = []  # 保存结果
n, m = 5, 10000
tracemalloc.start()
# 处理n个user的数据
for i in range(n):
    # 每个user造数据m条
    data = [[f'user{i}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)]
    data = np.array(sorted(data, key=lambda x: x[2]))[:10, :3]
    result.append(data)

    snapshot = tracemalloc.take_snapshot()  # 内存摄像
    top_stats = snapshot.statistics('lineno')  # 内存占用数据获取
    print(f'[Handle user{i} Top 3]')
    for stat in top_stats[:3]:  # 打印占用内存最大的10个子进程
        print(stat)

运行后会发现,这两种方法都无法解决问题,内存依然增长。再尝试使用copy()方法:

3、copy数组data

import time
import tracemalloc
import numpy as np

result = []  # 保存结果
n, m = 5, 10000
tracemalloc.start()
# 处理n个user的数据
for i in range(n):
    # 每个user造数据m条
    data = [[f'user{i}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)]
    new_data = np.array(sorted(data, key=lambda x: x[2]))[:10, :3].copy()
    result.append(new_data)

    snapshot = tracemalloc.take_snapshot()  # 内存摄像
    top_stats = snapshot.statistics('lineno')  # 内存占用数据获取
    print(f'[Handle user{i} Top 3]')
    for stat in top_stats[:3]:  # 打印占用内存最大的10个子进程
        print(stat)

 使用copy()后,即使n增大很多,内存也不会有明显增长。

接下来我们来分析一下内存会逐渐累积原因以及解决方案的原理。

Numpy的切片视图(View)

Numpy中对一个大数组进行切片提取时,速度可以非常快,是因为提取出的新变量只是创造了一个“View”,指向原来的数据,并没有将这部分数据复制一份出来。注意:这种视图模式只是切片的特性,如果使用列表进行提取,是会复制一份而不是提供了一个“View”。可以对比下面三段代码的速度:

Python-time.time() 和 time.perf_counter()_Rnan-prince的博客-CSDN博客

m = 100000
data = np.array([[f'user{888}', 'operation', int(time.time() * 1000), 'data1', 'data2', 'data3'] for j in range(m)])

# 1. 切片
s1 = time.perf_counter()
new_data1 = data[:m, :]
e1 = time.perf_counter()
print('==>', e1 - s1)

# 2. copy
s2 = time.perf_counter()
new_data2 = data[:m, :].copy()
e2 = time.perf_counter()
print('==>', e2 - s2)

# 3. 用列表提取
s3 = time.perf_counter()
new_data3 = data[range(m), :]
e3 = time.perf_counter()
print('==>', e3 - s3)

# ==> 1.430000000013365e-05
# ==> 0.005798200000000087
# ==> 0.10793530000000007

切片视图导致新变量和原变量指向相同的元素,因此在修改新变量中元素的值时,原来的变量值也会被修改。如下所示:

a = np.array([1, 2, 3])
b = a[:2]
b[0] = 10
print(a) # [10  2  3]

copy()和列表提取则b的改变不会影响a的值。

综上:“View”不是切片的特性,而是Numpy中切片的特性,list中的切片就默认带有copy()

示例的内存问题,因为new_data只是data的一个视图,所以即使data变量名被新的值覆盖掉,但被append了的new_data仍然指向原来的大数组,导致那个大数组无法被释放。如果之前的程序是用列表提取,不需要copy()也不会有内存问题:

  • 循环引用中两个对象都定义了__del__,这时垃圾回收也无法完成释放
  • 本该释放内存的对象被其他对象引用,造成内存没有被释放

引用计数

python的内存管理主要通过引用计数和垃圾回收完成。引用计数表示每个对象都对应一个计数器,当该对象对应的计数器为0的时候,对象的内存会被释放。一般引用计数可以处理大部分内存释放的问题,但它无法处理循环引用问题,这时就要通过垃圾回收来完成。python中的内存泄漏一般有两种原因:

  • 循环引用中两个对象都定义了__del__,这时垃圾回收也无法完成释放
  • 本该释放内存的对象被其他对象引用,造成内存没有被释放

当一个对象被创建时,计数器值为1。下面每一个事件都会导致一个对象的计数器的值减1

  • del a销毁变量别名
  • a=[]变量别名指向了其他对象
  • 对象离开定义时的作用域,比如函数中定义的局部变量,函数运行结束后引用计数减1
  • 对象所在容器被销毁

一般操作del或者l=[]都只是将引用计数器值减1,而很多时候它们的值本来就是1,所以减1就是0,是0它对应的内存就被释放了。下面是一些会使引用计数器值加1的事件

  • 对象被创建,如a = np.array([1, 2, 3])
  • 新的别名,b=a
  • 对象被放入容器中,l.append(a)
  • 对象作为参数被传入函数

为什么del和变量名覆盖没有解决内存泄露的问题?

>>> import sys
>>> a = np.array(range(10))
>>> sys.getrefcount(a) - 1
1

# numpy.切片
>>> a = np.array(range(10))
>>> b1 = a[:2]
>>> sys.getrefcount(a) - 1
2

# numpy.copy
>>> a = np.array(range(10))
>>> b2 = a[:2].copy()
>>> sys.getrefcount(a) - 1
1

# numpy.列表提取
>>> a = np.array(range(10))
>>> b3 = a[[1, 2]]
>>> sys.getrefcount(a) - 1
1

# 列表.切片
>>> a = list(range(10))
>>> d = a[:2]
>>> sys.getrefcount(l) - 1
1

sys.getrefcount(a) 减1的原因在于变量被传入这个函数导致了计数器加1 。

可以看出Numpy的切片会导致原始变量引用计数加1,其他方式都不会。这就是del无效的原因del只是降低了一次引用计数,并没有将它降低到0。变量名覆盖也是一样,原始变量名无论是被del还是被新的值覆盖,新的值一直都在引用着原始的对象,导致原始对象无法被释放。可以通过变量的base属性查看:

>>> a = np.array(range(10))
>>> b = a[:2]
>>> del a
>>> b.base
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

参考:

与NumPy有关的内存泄漏 - 知乎 

python - Numpy 数组索引与其他数组会产生广播错误 - IT工具网文章来源地址https://www.toymoban.com/news/detail-709615.html

到了这里,关于NumPy使用不当引起的内存泄漏的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Python 使用numpy.bincount计算混淆矩阵

    Confusion matrix using numpy.bincount. np.bincount 用于统计一个非负数组中元素的出现次数。函数格式如下: 通常默认数组 x x

    2024年02月07日
    浏览(42)
  • Python——第6章 Numpy库的使用

    Numpy 是 Python 专门处理高维数组 (high dimensional array) 的计算的包。 官网 (www.numpy.org). 列表和数组区别 列表:数据类型可以不同——3.1413, ‘pi’, 3.1404, [3.1401, 3.1349], ‘3.1376’ 数组:数据类型相同——3.1413, 3.1398, 3.1404, 3.1401, 3.1349, 3.1376 数组的创建—定隔定点的 np.aran

    2024年02月06日
    浏览(42)
  • Python numpy中random函数的使用

    np.random:随机数的生成 np.random.random() np.random.random(size) np.random.random([m,n])或np.random.random((m,n)) np.random.rand(m,n) 与np.random.random((m,n))作用一样,但是参数形式不同。 np.random.randint(a,b,size) np.random.uniform(a,b,size) np.random.normal():均值为0,标准差为1【无参默认值】 np.random.normal(a,b) n

    2023年04月08日
    浏览(37)
  • 【Python数据分析】numpy库的使用-上篇

    NumPy是一个用于科学计算的Python库,它提供了高性能的多维数组对象和用于处理这些数组的各种工具。NumPy的名称来自于“ Numerical Python ”的缩写。 NumPy的主要功能包括: 多维数组对象:NumPy提供了多维数组对象,称为 ndarray ,它是一个由同类型数据组成的表格。 ndarray 可以包

    2024年02月06日
    浏览(41)
  • Python Numpy 关于 linspace()函数 使用详解(全)

    用plt画图的时候,偶尔会看到这个函数的出现,索性直接深入源码实战进行复现 主要功能 :在线性区域中生成等间距的序列,原先在Numpy中可以用 numpy.arange() ,但对于浮点数会有精度丢失,因此 linspace() 对于浮点数比较友好。适当的参数,两者都可选择。 具体源码: numpy

    2024年02月05日
    浏览(50)
  • python 安装、配置、使用 xlrd模块、numpy模块、matplotlib、opencv模块

    目录  一、xlrd模块 (一)安装xlrd模块 (二) pycharm 配置xlrd (三) 读取xls格式 (四)xlrd读取时间日期时,会是float类型,需要转换。 二、numpy模块  (一)numpy模块安装---使用清华大学镜像进行安装即可 (二)pycharm配置时,numpy安装失败,且代码中出现 No module named \\\'numpy\\\' (1)

    2024年02月09日
    浏览(45)
  • Python数据分析:NumPy、Pandas和Matplotlib的使用和实践

    在现代数据分析领域中,Python已成为最受欢迎的编程语言之一。Python通过庞大的社区和出色的库支持,成为了数据科学家和分析师的首选语言。在Python的库中,NumPy、Pandas和Matplotlib是三个最为重要的库,它们分别用于处理数值数组、数据处理和可视化。本文将介绍这三个库的

    2024年02月04日
    浏览(70)
  • python中使用numpy包的向量矩阵相乘np.dot和np.matmul

    一直对np的线性运算不太清晰,正好上课讲到了,做一个笔记整个理解一下  在numpy中,一重方括号表示的是向量vector,vector没有行列的概念。二重方括号表示矩阵matrix,有行列。 代码显示如下: 即使[1,2,3]、[[1,2,3]]看起来内容一样 使用过程中也会有完全不一样的变化。下面

    2024年01月25日
    浏览(44)
  • 在Anaconda下安装并使用Pytorch,pillow,numpy等库及Python版本的匹配

    目录 1.在Anaconda Prompt创建新环境 2.去pytorch官网上查找环境中Python所对应的pytorch版本并下载相关包 查询电脑独立NVIDIA显卡所适配的CUDA版本 CUDA版本查询 3.部分库版本的安装与修改  本人在Anaconda下创建的新环境为 python 3.7.0 pytorch 1.8.0 pillow 9.5.0 numpy 1.21.5 能够正常运行 如果我这

    2024年02月15日
    浏览(44)
  • Python中数据处理(npz、npy、csv文件;元组、列表、numpy数组的使用)

    目录 1.npz文件 2.npy文件 3.csv文件 4.列表、元组、numpy矩阵 ①列表 ②元组(不可变列表) ③Numpy数组 ③Numpy矩阵 1.npz文件 npz是python的压缩文件 ①读取文件 ② NpzFile 对象有个属性 files 可以通过它查看该压缩文件的所有文件名,通过 dataset[\\\'文件名\\\'] 来获取文件内容 ③保存为.npz文件

    2024年02月03日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包