上篇文章中介绍了如何对代码性能进行分析优化,这篇文章将介绍如何对代码运行时内存进行分析。
说到内存,就想起之前在搞数据挖掘竞赛的时候,往往要跑很大的数据集,经常就是炸内存。当时的解决办法就是对着任务管理器用 jupyter notebook 分 cell 的跑代码,将需要耗费大量内存的代码块找出来,然后考虑各种方式进行优化。
这篇文章将会介绍些更好的方法,来对代码运行时内存进行分析,通过这些方法了解了代码的内存使用情况之后,我们可以思考:
能不能重写这个函数让它使用更少的 RAM 来工作得更有效率
我们能不能使用更多的 RAM 缓存来节省 CPU 时间
开始分析代码仍采用上篇文章中的
memory_profiler通过 pip install memory_profiler 来安装这个库。在需要进行分析的函数前加上修饰器 @profile
from memory_profiler import profile ... ... @profile def calculate_z_serial_purepython(maxiter, zs, cs): ... @profile def calc_pure_python(desired_width, max_itertions): ... ...然后命令行输入
python -m memory_profiler code_memory.py跑得十分的慢 - -,跑了一个多小时,输出如下
Length of x: 1000 Total elements: 1000000 Filename: code_memory.py Line # Mem usage Increment Line Contents ================================================ 30 159.1 MiB 159.1 MiB @profile 31 def calculate_z_serial_purepython(maxiter, zs, cs): 32 166.7 MiB 7.6 MiB output = [0] * len(zs) 33 166.7 MiB 0.0 MiB for i in range(len(zs)): 34 166.7 MiB 0.0 MiB n = 0 35 166.7 MiB 0.0 MiB z = zs[i] 36 166.7 MiB 0.0 MiB c = cs[i] 37 166.7 MiB 0.0 MiB while n < maxiter and abs(z) < 2: 38 166.7 MiB 0.0 MiB z = z * z + c 39 166.7 MiB 0.0 MiB n += 1 40 166.7 MiB 0.0 MiB output[i] = n 41 108.3 MiB 0.0 MiB return output calculate_z_serial_purepython took 8583.605925321579 seconds Filename: code_memory.py Line # Mem usage Increment Line Contents ================================================ 43 80.9 MiB 80.9 MiB @profile 44 def calc_pure_python(desired_width, max_itertions): 45 80.9 MiB 0.0 MiB x_step = (float(x2 - x1)) / float(desired_width) 46 80.9 MiB 0.0 MiB y_step = (float(y2 - y1)) / float(desired_width) 47 80.9 MiB 0.0 MiB x, y = [], [] 48 80.9 MiB 0.0 MiB ycoord = y1 49 80.9 MiB 0.0 MiB while ycoord < y2: 50 80.9 MiB 0.0 MiB y.append(ycoord) 51 80.9 MiB 0.0 MiB ycoord += y_step 52 80.9 MiB 0.0 MiB xcoord = x1 53 80.9 MiB 0.0 MiB while xcoord < x2: 54 80.9 MiB 0.0 MiB x.append(xcoord) 55 80.9 MiB 0.0 MiB xcoord += x_step 56 80.9 MiB 0.0 MiB zs, cs = [], [] 57 159.1 MiB 0.0 MiB for ycoord in y: 58 159.1 MiB 0.1 MiB for xcoord in x: 59 159.1 MiB 0.9 MiB zs.append(complex(xcoord, ycoord)) 60 159.1 MiB 0.1 MiB cs.append(complex(c_real, c_imag)) 61 159.1 MiB 0.0 MiB print(f"Length of x: {len(x)}") 62 159.1 MiB 0.0 MiB print(f"Total elements: {len(zs)}") 63 159.1 MiB 0.0 MiB start_time = time.time() 64 108.6 MiB 0.0 MiB output = calculate_z_serial_purepython(max_itertions, zs, cs) 65 108.6 MiB 0.0 MiB end_time = time.time() 66 108.6 MiB 0.0 MiB secs = end_time - start_time 67 108.6 MiB 0.0 MiB print("calculate_z_serial_purepython took", secs, "seconds") 68 69 108.6 MiB 0.0 MiB assert sum(output) == 33219980可以看到:
第 32 行,可以看到分配了 1000000 个项目,导致大约 7M 的 RAM 被加入这个进程
在 57 行的父进程中,可以看到 zs 和 cs 列表的分配占用了大约 70M。
注:这里的的数字并不一定是数组的真实大小,只是进程在创建这些列表的过程中增长的大小
mprof在 memory_profiler 库中,还有一种通过随时间进行采样并画图的方式来展示内存使用变化,叫 mprof。
记得把 @profile 注释掉
mprof run code_memory.py运行结束后会有一个 .dat 文件,接着命令行输入
mprof plot生成图片
这个图看起来好像还不是很直观,并不能看出内存增长是在哪里,修改下函数,这里还要把 from memory_profiler import profile 注释掉
def calculate_z_serial_purepython(maxiter, zs, cs): with profile.timestamp("create_output_list"): output = [0] * len(zs) time.sleep(1) with profile.timestamp("create_range_of_zs"): iterations = range(len(zs)) with profile.timestamp('calculate_output'): for i in iterations: n = 0 z = zs[i] c = cs[i] while n < maxiter and abs(z) < 2: z = z * z + c n += 1 output[i] = n return output然后命令行
mprof run code_memory.py画图
memit类似于运行时间测量的 timeit,内存测量中也有 memit,可在 ipython 或 jupyter notebook 中使用