python 解释器有很多:CPython、IPython、Jython、PyPy,这些解释器由编译器和虚拟机组成。
虚拟机可以让python的编程者无需关注底层实现(比如要如何为数组分配内存、如何组织内存以及用什么样的顺序将内存传入 CPU),好处是可以直接快速设计出更高层的业务逻辑和算法,缺陷则是要付出性能损失的代价。但python指令层本身就存在优化,所以更好的利用这一层优化(即使用正确的指令顺序),也就可以提高你写的python性能。
其次,python的GIL(全局解释器锁)会影响程序在并行方面的性能。GIL是CPython中的一个概念,它通过计数的方式进行内存管理,实现了一个互斥锁(防止多线程并发执行机器码)。那么在多核CPU上运行时python时,实际用到的可能就是一个单核,其他沦为摆设。如下图所示,三个线程都只有等到前一个释放资源后才能继续运行。因此需要使用标准库的 multiprocessing,numexpr,或分布式计算模型等方法来解决。
GIL 确保 Python 进程一次只能执行一条指令,无论当前有多少个核心。 这意味着即使某些 Python代码可以使用多个核心,在任意时间点仅有一个核心在执行 Python 的指令。
另一方面,Python 使用了动态类型,且 Python 也并不是一门编译性的
语言。由于代码在运行过程中会发生改变,那么也没办法在编译器层面对代码进行优化。但使用Just-In-Time(JIT)技术可以改善这一问题,实现加速。例如CPython中将python代码注释为C语言类型,还有微软的Pyston等。
time计时
import time
start_time = time.time()
for i in range(1,1000000):
x = i*i
end_time = time.time()
print("Spend:{:.4f}".format(end_time-start_time) )
结果:
Spend:0.0618
装饰器
可以定义一个装饰器来自动测量时间:
from functools import wraps
def timefn(fn):
@wraps(fn)
def measure_time(*args, **kwargs):
t1 = time.time()
使用装饰器:
@timefn
def calculate_z_serial_purepython(maxiter, zs, cs):
timeit模块
该模块会禁用垃圾回收机制。 命令行中使用-m timeit
的方式就可以调用
指定-n
循环次数和-r
重复次数,如果不指定则默认为n=10,r=5。
python -m timeit -n 5 -r 5 -s "import julia1" "julia1.calc_pure_python(desired_width=1000,
max_iterations=300)"
UNIX 的 time
调用python脚本时,命令行前加上 /usr/bin/time -p
,使用系统的time。但只能在类UNIX系统下使用。
python脚本:test.py
import time
start_time = time.time()
for i in range(1,1000000):
x = i*i
end_time = time.time()
print("Finish test.")
运行:/usr/bin/time --verbose python test.py
打开--verbose 开关可以获得更多输出信息
总共有三个:
后两者接口是一致的,实现方法不同。profile是纯python实现,而cProfile用C语言钩入 CPython 的虚拟机来测量其每一个函数运行所花费的时间(代价巨大但信息更丰富)。
例子:
import cProfile
import re
cProfile.run('re.compile("foo|bar")')
打印信息:
197 function calls (192 primitive calls) in 0.002 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.001 0.001 :1()
1 0.000 0.000 0.001 0.001 re.py:212(compile)
1 0.000 0.000 0.001 0.001 re.py:268(_compile)
1 0.000 0.000 0.000 0.000 sre_compile.py:172(_compile_charset)
1 0.000 0.000 0.000 0.000 sre_compile.py:201(_optimize_charset)
4 0.000 0.000 0.000 0.000 sre_compile.py:25(_identityfunction)
3/1 0.000 0.000 0.000 0.000 sre_compile.py:33(_compile)
分析工具
为了更好的分析cProfile得到的结果,可以使用这些模块。
import pstats
# 创建Stats对象
p = pstats.Stats("result.out")
# strip_dirs(): 去掉无关的路径信息
# sort_stats(): 排序,支持的方式和上述的一致
# print_stats(): 打印分析结果,可以指定打印前几行
# 和直接运行cProfile.run("test()")的结果是一样的
p.strip_dirs().sort_stats(-1).print_stats()
# 按照函数名排序,只打印前3行函数的信息, 参数还可为小数,表示前百分之几的函数信息
p.strip_dirs().sort_stats("name").print_stats(3)
# 按照运行时间和函数名进行排序
p.strip_dirs().sort_stats("cumulative", "name").print_stats(0.5)
# 如果想知道有哪些函数调用了sum_num
p.print_callers(0.5, "sum_num")
# 查看test()函数中调用了哪些函数
p.print_callees("test")
line_profiler可以进行逐行分析
pip
或者conda
下载line_profiler包以后,用@profile
装饰器的方式使用。
用修饰器(@profile)标记选中的函数。用 kernprof.py 脚本运行你的代码,被选函数每一行花费的 CPU 时间以及其他信息就会被记录下来。
命令行中运行 kernprof 逐行分析被修饰函数的 CPU 开销:
kernprof -l -v test.py
运行时参数-l 代表逐行分析而不是逐函数分析,-v 用于显示输出。没有-v,你会
得到一个.lprof 的输出文件,回头你可以用 line_profiler 模块对其进行分
析。例 2-6 中,我们会完整运行一遍我们的 CPU 密集型函数
memory_profiler可以诊断内存的用量,操作与上一个包类似,也要先添加@profile
在你需要诊断的函数上方,然后运行:
python -m memory_profiler test.py
再通过mprof功能,将生成的统计文件制作成图。
1、如果觉得更改代码,每次都要去添加@profile很麻烦,可以考虑使用no-op 修饰器,避免出现Import Error之类的引用错误。
例如:
# memory_profiler
if 'profile' not in dir():
def profile(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
2、要保证测试机器的稳定,例如在 BIOS 上禁用了 TurboBoost,禁用操作系统改写 SpeedStep,不要用笔记本电池而是使用主电源。
3、多次测试,备份数据。
[1] Python高性能编程
[2] Realpython
[3] 一份让Python疯狂加速的工具合集!
[4] python性能分析之cProfile模块
[5]The Python Profilers