Python性能调优:copy模块中的deepcopy函数
最近在写的一些Python小项目中,里面用到了遗传算法这种需要大量计算的算法(CPU密集型任务),因为GIL锁的存在,Python中的多线程在对CPU密集型任务的处理中显得有些鸡肋,于是尝试采用multiprocessing(多进程)模块来就行计算任务处理,但提升有限。

关于GIL
GIL(Global Interpreter Lock)是Python解释器中的一个全局锁,用于确保在任何时刻只有一个线程在执行Python字节码,一定程度上避免了冒险-竞争问题。这意味着,尽管Python支持多线程编程,但在任何时刻只有一个线程可以在解释器中执行Python代码。如果有多个线程需要同时执行Python代码,则它们必须在GIL上进行竞争,获得GIL的线程可以运行Python代码,而其他线程则必须等待GIL。

问题查找
做性能优化时,首先要找出性能的瓶颈在什么地方,才能够做针对性的优化。Python 的性能剖析主要有下面几种方法:
cProfile
line_profiler
pyflame
pyinstrument
在本文章中将使用Python内置的cProfile模块,cProfile是Python标准库中的一个性能分析工具,用于分析Python程序的性能瓶颈。它可以统计程序中每个函数的调用次数、运行时间以及占用的内存等信息,并将这些信息以一定格式打印出来。
与Python内置的profile模块不同,cProfile使用了C语言实现的底层分析引擎,因此可以大大减少分析工具本身对程序性能的影响,同时提供更为精确的分析结果。
官方例子(命令行使用):
其中-o 选项指定生成 cProfile 的二进制性能结果保存至指定地址。
光生成二进制结果还不够,这时就需要一些更直观的方式:使用flameprof生成svg可视化火焰图
由于是外置库,首先使用pip进行安装:
假设上面使用cProfile生成的二进制文件名为input.prof:
执行命令即可获得该程序的火焰图。

可以明显看出,时间主要用在了deepcopy这个函数上。该函数主要功能是实现对象的深拷贝。
查阅cpython源码(https://github.com/python/cpython/blob/main/Lib/copy.py)发现,这个函数会递归地遍历输入对象的所有子对象,并复制它们以创建一个新的独立对象。它使用一个名为memo
的字典来跟踪已经被复制的对象,避免对同一个对象进行重复的复制。关键就在这个memo(全名memorandum)的字典变量,该函数需要实时维护这个字典,故性能会有所下降。但在绝大多数情况下,程序里都不存在相互引用。但作为通用模块的copy模块必须考虑这种特殊情况。
解决方法
自定义deepcopy方法:
重新生成火焰图:

分析上图,时间大部分都在运行numpy计算,而不是deepcopy,而每秒迭代次数得到了300%的巨大提升。