以下是一段会引起内存逐步累积的代码:
代码大意:处理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”,指向原来的数据,并没有将这部分数据复制一份出来。注意:这种视图模式只是切片的特性,如果使用列表进行提取,是会复制一份而不是提供了一个“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=[]
变量别名指向了其他对象一般操作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工具网