CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

高性能Python(一)程序性能分析方法-免费源码丞旭猿

新年好。这里介绍一套行之有效的优化思维方式,以应对工作上的python程序效率提升问题。

总的来说,性能分析一般是CPU占用、内存占用和IO这三个维度。下面列出了一些常见状况的应对思路,可以”代号入座”进行性能分析,选择最具有时间性价比的方向进行优化。

实际工作中,一个程序很难做到完美,我们也不希望花费过多精力做较少的边际优化。本文讨论的性能优化的范畴,是使用较少时间将不可接受的性能转换为令人满意的性能,不追求做到完美。

优化CPU占用

大部分计算密集性程序都能从算法和数据结构层面解决,但是这对于程序的编写者提出了比较高的要求。这部分不是本系列专题讨论的主要内容,因为这样的优化需要根据程序本身需要解决的问题来实现。并且数据结构和算法的改变带来的编码时间成本很高,在很多需要开发人员有效率解决实际问题的情形下,这种时间代价往往让人无法接受。而某一些情景下,算法层面已经无法再进行优化了,程序的瓶颈转变为python这种动态解释性语言本身的瓶颈。这时候编译代码,就成为性价比最高的一种提升效率的方式。无需改动代码本身,就能够获得成倍的性能提升。

目前常用的编译python代码的工具包括:Cython,PyPy,Shed Skin,即时编译的Numba。

这些工具我们将在后续的文章中逐一进行介绍。

优化内存使用

高性能的程序在内存使用方面需要做到两个方面:高效的内存访问和较低的内存占用。在使用内存的时候,有一些提高内存使用效率的细节值得注意:

  • 访问连续的内存地址,比起访问随机的内存地址要更加高效
  • 避免内存复制
  • 预先分配内存在大部分时候更高效

内存本身也是有组织结构的(这里把CPU的cache也看成广义的内存),分为堆和栈两种形式:

  • 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。栈使用的是一片较小的连续地址。
  • 堆(操作系统):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定。所以调用这些对象的速度较慢。堆使用的是不连续的地址,大小由计算机的虚拟内存决定。

总的来说,使用速度越快的内存(L1>L2>L3>内存>磁盘>网络),内存间通信越少,内存访问效率越高,程序的运行速度也就越快。

内存复制的代价较高昂,在python的设计中处处体现了对内存复制的规避。以list为例,因为list是可变对象,如果每次append都涉及到对原有对象的销毁和新对象的创建,效率就会很低。cpython在设计list占用空间的时候满足如下的公式:

转换过来的关系是:

也就是说在我们len为1000的列表实际占用的空间是1120。这样设计的主要目的就是为了避免list长度改变时不停进行内存复制,只有当此列表长度增长超过1120的时候才会产生一次内存复制,以提升程序性能。

反过来,如果我们不需要可变的list的时候,直接创建一个静态不可变的tuple效率会更高。笔记本上二者的效率差异如下:创建list大概为tuple耗时的3.7倍。

优化IO开销

对于IO密集型的程序而言,优化IO效率就显得非常有必要。

假设主要的IO数据主要存在于磁盘上,可以尝试进行上图中的一些分析,从而找到优化IO的突破口。最容易想到的就是采用多核并行处理数据,直接倍增IO效率,但是在多进程同时写一个文件的时候需要注意加锁,否则会产生严重的后果(可直接使用Modin dask一类的工具)。

其次,如果程序处理的数据是否可以进行预处理,利用主要程序运行前的时间生成一份中间数据,能够大大加速最终主要程序的运行时间。

如果上述都不行,则可以考虑使用更为高效的IO接口,例如pandas在面对大型数据集的时候效率可能不如numpy,甚至不如使用with open打开,缺点则是需要多写部分代码。另外数据储存的格式也是很关键的,选择合适文件格式对提升IO效率也有帮助,对于同一种文件来说,选择合适数据类型也有利于合适使用磁盘空间,减少程序运行时的内存占用,从而间接提升程序性能。

最后,如果涉及到不同进程的管理,最好使用异步非阻塞的形式。

分析你的代码性能

分析python代码性能是优化的先决条件。下面简单介绍一下帮助性能分析的工具:

>系统工具(Linux)

  • top
  • htop
  • glances

tophtop都是常用的系统级的性能分析工具,通过top可以查看出用户每一个进程的内存和cpu占用。而他的兄弟htop能够显示机器每一个核心的使用情况,还能够显示出哪个用户的哪一条语句占用了多少cpu和内存资源,非常直观。下图来自geekforgeeks:

top 展示出来的各项指标的含义都非常丰富,总的来说包含如下这些:

术语    含义
PID     进程id
PPID    父进程id
USER    进程所有者的用户名
PR      优先级
NI      nice值。负值表示高优先级,正值表示低优先级
P       最后使用的CPU,仅在多CPU环境下有意义
%CPU    上次更新到现在的CPU时间占用百分比
TIME    进程使用的CPU时间总计,单位秒
TIME+   进程使用的CPU时间总计,单位1/100秒
%MEM    进程使用的物理内存百分比
VIRT    进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
SWAP    进程使用的虚拟内存中,被换出的大小,单位kb。
RES     进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
SHR     共享内存大小,单位kb
S       进程状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
COMMAND 命令名/命令行

查看任务时,可以按M,看内存占用(RES/MEM),按P,看 CPU 占用。另外,按组合键xb,然后用<>手动选择排序的列,这样查看起来更自由。

htop的功能类似,不过htop有了图形界面看起来更酷:

glances则是集大成者,作为一款python开发的系统性能检测工具,除了集成top,htop的功能之外,glances还能查看系统io,网络通信等多达15项指标,如果有GPU的话也能被一起监控:

除此之外,glances能更方便地导出一段时间内的系统性能检测指标,方便实现实时监控,结合grafanaprometheus等工具可以实现实时监控的可视化以及及时预警。但glances也不是没有缺点,由于监测的性能指标比较多,glances整体比较消耗资源。

当然linux提供的性能分析工具远不止这些。另一套性能分析的组合拳则是pstack,straceperfpstack会显示程序的线程函数调用栈,strace则提供了对程序运行的动态追踪,perf则可以分析各种内核级别的问题。这几个工具可以用来分析一切程序,对python的针对性没有那么强,并且有一定的使用门槛。有兴趣的读者可以自行查询相关资料了解。

>Python性能分析器

说完了系统级别的性能分析,再来说一说如何找到具体拖累程序性能的代码。对于python代码来说常用的逐行计时分析工具主要包含以下三个,其中cProfile已经作为标准库被纳入,:

  • cProfile
  • line_profiler
  • PyInstrument

cProfile

使用cProfile无需改动源码,只需要运行代码时,在命令行加上参数即可:

python-mcProfile-scumulativexxx.py

PyCharm中可以直接使用这一工具,无需在命令行中指定。下面以一段蒙特卡洛模拟计算圆周率的小程序来展示cProfile的功能。程序非常简单,这里我使用了argparse这个库来设置命令行参数:

.\cal_pi.pyimportargparseimportnumpyasnpdefcal_pi(n_sims):in_circle=0forisimsinrange(n_sims):x,y=np.random.rand(2)ifx**2+y**2<1:in_circle+=1pi=in_circle/n_sims*4returnpiif__name__=="__main__":parser=argparse.ArgumentParser()parser.add_argument(-n,--n_sims,type=(int))args=parser.parse_args()pi=cal_pi(args.n_sims)print(f"pi is {pi}")

运行此代码,可以获得如下的结果(省略)

PS D:\work> python -m cProfile -s cumulative .\cal_pi.py -n 10000000
pi is 3.1417672
         10075583 function calls (10073367 primitive calls) in 41.189 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    437/1    0.001    0.000   41.190   41.190 {built-in method builtins.exec}
        1    0.000    0.000   41.190   41.190 cal_pi.py:2()
        1   30.058   30.058   41.030   41.030 cal_pi.py:10(cal_pi)
 10000000   10.972    0.000   10.972    0.000 {method rand of numpy.random.mtrand.RandomState objects}
       12    0.001    0.000    0.292    0.024 __init__.py:1()
    153/2    0.001    0.000    0.158    0.079 :986(_find_and_load)
    153/2    0.001    0.000    0.158    0.079 :956(_find_and_load_unlocked)
    144/2    0.001    0.000    0.156    0.078 :650(_load_unlocked)
    111/2    0.000    0.000    0.156    0.078 :777(exec_module)
    222/2    0.000    0.000    0.156    0.078 :211(_call_with_frames_removed)
   188/16    0.001    0.000    0.135    0.008 :1017(_handle_fromlist)
    319/8    0.000    0.000    0.134    0.017 {built-in method builtins.__import__}
      150    0.001    0.000    0.038    0.000 :890(_find_spec)
      138    0.000    0.000    0.037    0.000 :1334(find_spec)
      138    0.001    0.000    0.036    0.000 :1302(_get_spec)
      456    0.004    0.000    0.033    0.000 :1431(find_spec)
      111    0.001    0.000    0.029    0.000 :849(get_code)
      755    0.000    0.000    0.027    0.000 :80(_path_stat)
      756    0.027    0.000    0.027    0.000 {built-in method nt.stat}

如果希望保存上述结果,先执行命令-o,保存性能分析文件到指定的路径:

python-mcProfile-oprofile.stats.\cal_pi.py-n100000

可以使用pstats库对此文件进行分析:

importpstatsp=pstats.Stats("profile.stats")p.sort_stats("cumulative")p.print_stats()展示函数调用时间分析结果p.print_callers()展示函数的被调用情况p.print_callees()展示函数的调用情况

可以看出cProfile是一个功能非常强大的库,可以显示很多程序运行的细节,方便查找函数的调用和被调用的关系,也可显示出调用的次数信息。对比较复杂的程序调用cProfile分析可能非常有用,但对于本例的简单程序而言,略有大炮打蚊子的嫌疑——过多的信息量反而淹没了重要信息,cProfile显示了太多的细节,导致信息过剩,不能一下子关注到问题的焦点。(从结果中可以看出整体最耗时的部分就是随机数生成的部分,不具有太大的优化空间。)

line_profiler

line_profiler输出的结果更为简洁:在函数前加上装饰器@profile通过命令行执行:

PS D:\work> kernprof -l -v .\cal_pi.py -n 1000000
pi is 3.141776
Wrote profile results to cal_pi.py.lprof
Timer unit: 1e-06 s

Total time: 5.69313 s
File: .\cal_pi.py
Function: cal_pi at line 10

Line       Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    10                                           @profile
    11                                           def cal_pi(n_sims):
    12         1          2.1      2.1      0.0      in_circle = 0
    13   1000001     411368.5      0.4      7.2      for isims in range(n_sims):
    14   1000000    3032114.9      3.0     53.3          x, y = np.random.rand(2)
    15   1000000    1841899.0      1.8     32.4          if x**2 + y**2 < 1:
    16    785444     407741.1      0.5      7.2              in_circle += 1
    17         1          2.1      2.1      0.0      pi = in_circle/n_sims*4
    18         1          0.9      0.9      0.0      return pirn pi

整个过程就变得非常直观,随机数生成占用了53%的时间,判断是否在圆内占用了32.8%的时间。如果想要进一步优化仅需要从这两个地方入手,另外注意循环这里其实也占用了大约7%的时间,如果能规避循环说不定也能对程序性能有较好的提升。

PyInstrument

接下来介绍PyInstrument。和line_profiler类似,PyInstrument也能提供逐行的分析结果,不过作为一名性能分析的新秀,PyInstument的颜值可是比他的两位兄弟要高不少。PyInstrument提供了修改源码和不改源码两种分析方式,修改源码可以针对性对大型项目中的指定的代码片段进行分析,这一点比使用装饰器的line_profiler更加灵活(毕竟装饰器只能放在函数前面)。

把我们的原始代码,适当包裹一下:

importargparseimportnumpyasnpfrompyinstrumentimportProfilerdefcal_pi(n_sims):in_circle=0forisimsinrange(n_sims):x,y=np.random.rand(2)ifx**2+y**2<1:in_circle+=1pi=in_circle/n_sims*4returnpiif__name__=="__main__":parser=argparse.ArgumentParser()parser.add_argument(-n,--n_sims,type=(int))args=parser.parse_args()profiler=Profiler()profiler.start()pi=cal_pi(args.n_sims)print(f"pi is {pi}")profiler.stop()print(profiler.output_text(unicode=True,color=True))

python执行此脚本可以得到如下的结果:

如果使用不修改源码的方式执行:

打开其中的-r参数,则可以加载可视化报告:

PyInstrument还提供了IPython的魔术方法支持,直接在cell中加上%%pyinstrument即可,对喜欢使用IPython/Jupyter的用户来说非常方便。

memory_profiler

memory_profilerline_profiler的使用一样,在需要被分析的函数前加上@profile装饰其执行命令行即可:

PS D:\work> python -m memory_profiler .\cal_pi.py -n 10000
pi is 3.1344
Filename: .\cal_pi.py

Line     Mem usage    Increment  Occurrences   Line Contents
=============================================================
    10   50.559 MiB   50.559 MiB           1   @profile
    11                                         def cal_pi(n_sims):
    12   50.559 MiB    0.000 MiB           1       in_circle = 0
    13   50.594 MiB    0.000 MiB       10001       for isims in range(n_sims):
    14   50.594 MiB    0.023 MiB       10000           x, y = np.random.rand(2)
    15   50.594 MiB    0.012 MiB       10000           if x**2 + y**2 < 1:
    16   50.594 MiB    0.000 MiB        7836               in_circle += 1
    17   50.594 MiB    0.000 MiB           1       pi = in_circle/n_sims*4
    18   50.594 MiB    0.000 MiB           1       return pi

可以看出整体程序占用内存大概在50.59MiB(1MiB=1.05MB)左右。这里我将n设置为10000,不得不说memory_profiler尽管用起来简单,但是对程序的拖累不是一点半点,整个程序的运行耗时会被大大增加,不太建议对于过于复杂的程序进行分析。

参考文献

  1. PyInstrument文档
  2. Python高性能编程

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容