以下是一段会引起内存逐步累积的代码:
代码大意:处理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])
参考: