Python性能分析

发布时间:2022-04-22 09:57:32 人气:34 作者:多测师

  Python标准库提供两个代码性能分析相关的模块,即timeit和cProfile/profile。前者更适合测试简短的代码片段,后者则可分析代码片段乃至整体模块中各个函数的调用次数、运行耗时等信息。

  cProfile是profile的C版本,开销更小。基于cProfile模块,可方便地评估程序性能瓶颈(bottleneck),借以发现程序中值得优化的短板。

  根据粒度不同,可将cProfile使用场景分为三类。

  1.1 分析单条语句

  import cProfile, pstats, re, cStringIO

  cProfile.run('re.compile("foo|bar")', 'prfRes') #将cProfile的结果写入prfRes文件

  p = pstats.Stats('prfRes') #pstats读取cProfile输出结果

  #strip_dirs()剥除模块名的无关路径(如C:\Python27\lib\)

  #sort_stats('cumtime')或sort_stats('cumulative')按照cumtime对打印项排序

  #print_stats(n)打印输出前10行统计项(不指定n则打印所有项)

  p.strip_dirs().sort_stats('cumtime').print_stats(5)

  pstats 模块可用多种方式对cProfile性能分析结果进行排序并输出。运行结果如下:

  Tue May 24 13:56:07 2016 prfRes

  195 function calls (190 primitive calls) in 0.001 seconds

  Ordered by: cumulative time

  List reduced from 33 to 5 due to restriction <5>

  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:192(compile)

  1 0.000 0.000 0.001 0.001 re.py:230(_compile)

  1 0.000 0.000 0.001 0.001 sre_compile.py:567(compile)

  1 0.000 0.000 0.000 0.000 sre_compile.py:552(_code)

  其中,tottime表示某函数的总计运行时间(不含该函数内调用的子函数运行时间),cumtime表示某函数及其调用的子函数的累积运行时间。

  1.2 分析代码片段

  pr = cProfile.Profile()

  pr.enable() #以下为待分析代码段

  regMatch = re.match('^([^/]*)/(/|\*)+(.*)$', '//*suspicious')

  print regMatch.groups()

  pr.disable() #以上为待分析代码段

  s = cStringIO.StringIO()

  pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(10)

  print s.getvalue()

  运行结果如下:

  ('', '*', 'suspicious')

  536 function calls (512 primitive calls) in 0.011 seconds

  Ordered by: cumulative time

  List reduced from 78 to 10 due to restriction <10>

  ncalls tottime percall cumtime percall filename:lineno(function)

  2 0.000 0.000 0.009 0.005 C:\Python27\lib\idlelib\PyShell.py:1343(write)

  2 0.000 0.000 0.009 0.005 C:\Python27\lib\idlelib\rpc.py:591(__call__)

  2 0.000 0.000 0.009 0.005 C:\Python27\lib\idlelib\rpc.py:208(remotecall)

  2 0.000 0.000 0.009 0.004 C:\Python27\lib\idlelib\rpc.py:238(asyncreturn)

  2 0.000 0.000 0.009 0.004 C:\Python27\lib\idlelib\rpc.py:279(getresponse)

  2 0.000 0.000 0.009 0.004 C:\Python27\lib\idlelib\rpc.py:295(_getresponse)

  2 0.000 0.000 0.009 0.004 C:\Python27\lib\threading.py:309(wait)

  8 0.009 0.001 0.009 0.001 {method 'acquire' of 'thread.lock' objects}

  1 0.000 0.000 0.002 0.002 C:\Python27\lib\re.py:138(match)

  1 0.000 0.000 0.002 0.002 C:\Python27\lib\re.py:230(_compile)

Python性能分析

  1.3 分析整个模块

  使用命令行,调用cProfile脚本分析其他脚本文件。命令格式为:

  python -m cProfile [-o output_file] [-s sort_order] myscript.py

  注意,-o和-s选项不可同时使用。

  以C代码统计工具为例,运行如下命令:

  E:\PyTest>python -m cProfile -s tottime CLineCounter.py source -d -b > out.txt

  截取out.txt文件部分内容如下:

  2503 1624 543 362 0.25 xtm_mgr.c

  140872 93749 32093 16938 0.26

  762068 function calls (762004 primitive calls) in 2.967 seconds

  Ordered by: internal time

  ncalls tottime percall cumtime percall filename:lineno(function)

  82 0.985 0.012 2.869 0.035 CLineCounter.py:11(CalcLines)

  117640 0.612 0.000 1.315 0.000 re.py:138(match)

  117650 0.381 0.000 0.381 0.000 {method 'match' of '_sre.SRE_Pattern' objects}

  117655 0.319 0.000 0.324 0.000 re.py:230(_compile)

  138050 0.198 0.000 0.198 0.000 {method 'isspace' of 'str' objects}

  105823 0.165 0.000 0.165 0.000 {method 'strip' of 'str' objects}

  123156/123141 0.154 0.000 0.154 0.000 {len}

  37887 0.055 0.000 0.055 0.000 {method 'group' of '_sre.SRE_Match' objects}

  82 0.041 0.000 0.041 0.000 {method 'readlines' of 'file' objects}

  82 0.016 0.000 0.016 0.000 {open}

  1 0.004 0.004 2.950 2.950 CLineCounter.py:154(CountDir)

  由tottime可见,此处的性能瓶颈在于CalcLines()函数和其中的正则表达式处理。而isspace()和strip()方法及len()函数因调用次数较多,总的耗时也颇为可观。

  作为对比,以下给出一种未使用正则表达式的统计实现:

  def CalcLines(line, isBlockComment):

  lineType, lineLen = 0, len(line)

  line = line + '\n' #添加一个字符防止iChar+1时越界

  iChar, isLineComment = 0, False

  while iChar < lineLen:

  #行结束符(Windows:\r\n; Mac:\r; Unix:\n)

  if line[iChar] == '\r' or line[iChar] == '\n':

  break

  elif line[iChar] == ' ' or line[iChar] == '\t': #空白字符

  iChar += 1; continue

  elif line[iChar] == '/' and line[iChar+1] == '/': #行注释

  isLineComment = True

  lineType |= 2; iChar += 1 #跳过'/'

  elif line[iChar] == '/' and line[iChar+1] == '*': #块注释开始符

  isBlockComment[0] = True

  lineType |= 2; iChar += 1

  elif line[iChar] == '*' and line[iChar+1] == '/': #块注释结束符

  isBlockComment[0] = False

  lineType |= 2; iChar += 1

  else:

  if isLineComment or isBlockComment[0]:

  lineType |= 2

  else:

  lineType |= 1

  iChar += 1

  return lineType #Bitmap:0空行,1代码,2注释,3代码和注释

  在CalcLines()函数中。参数line为当前文件行字符串,参数isBlockComment指示当前行是否位于块注释内。该函数直接分析句法,而非模式匹配。注意,行结束符可能因操作系统而异,因此应区分CR(回车)和LF(换行)符。此外,也可在读取文件时采用"rU"(即通用换行模式),该模式会将行结束符\r\n和 \r替换为\n。

  基于新的CalcLines()函数,CountFileLines()函数需作如下修改:

  def CountFileLines(filePath, isRawReport=True, isShortName=False):

  fileExt = os.path.splitext(filePath)

  if fileExt[1] != '.c' and fileExt[1] != '.h':

  return

  isBlockComment = [False] #或定义为全局变量,以保存上次值

  lineCountInfo = [0]*4 #[代码总行数, 代码行数, 注释行数, 空白行数]

  with open(filePath, 'r') as file:

  for line in file:

  lineType = CalcLines(line, isBlockComment)

  lineCountInfo[0] += 1

  if lineType == 0: lineCountInfo[3] += 1

  elif lineType == 1: lineCountInfo[1] += 1

  elif lineType == 2: lineCountInfo[2] += 1

  elif lineType == 3: lineCountInfo[1] += 1; lineCountInfo[2] += 1

  else:

  assert False, 'Unexpected lineType: %d(0~3)!' %lineType

  if isRawReport:

  global rawCountInfo

  rawCountInfo[:-1] = [x+y for x,y in zip(rawCountInfo[:-1], lineCountInfo)]

  rawCountInfo[-1] += 1

  elif isShortName:

  detailCountInfo.append([os.path.basename(filePath), lineCountInfo])

  else:

  detailCountInfo.append([filePath, lineCountInfo])

  将这种统计实现命名为BCLineCounter.py。通过cProfile命令分析其性能,截取out.txt文件部分内容如下:

  2503 1624 543 362 0.25 xtm_mgr.c

  140872 93736 32106 16938 0.26

  286013 function calls (285979 primitive calls) in 3.926 seconds

  Ordered by: internal time

  ncalls tottime percall cumtime percall filename:lineno(function)

  140872 3.334 0.000 3.475 0.000 BCLineCounter.py:15(CalcLines)

  83 0.409 0.005 3.903 0.047 BCLineCounter.py:45(CountFileLines)

  141593/141585 0.142 0.000 0.142 0.000 {len}

  82 0.014 0.000 0.014 0.000 {open}

  1 0.004 0.004 0.004 0.004 collections.py:1()

  416 0.003 0.000 0.004 0.000 ntpath.py:96(splitdrive)

  84 0.002 0.000 0.002 0.000 {nt._isdir}

  1 0.002 0.002 0.007 0.007 argparse.py:62()

  1 0.002 0.002 3.926 3.926 BCLineCounter.py:6()

  可见,性能并不如CLineCounter.py。因此,使用标准库(如re)提供的函数或方法,不失为明智的选择。

  此外,对比BCLineCounter.py和CLineCounter.py的详细行数报告可知,两者的统计结果存在细微差异(正负误差不超过5行)。差异主要体现在有效代码行和纯注释行统计上,因为总行数和空白行数通常不会出现统计误差。那么,哪种实现更可靠呢?

  作者首先想到挑选存在统计差异的文件,人工或半人工地删除纯注释行和空白行,从而得到精确的有效代码行数。之所以不编写脚本自动删除上述类型的文件行,是因为作者对于注释行的解析已经存在误差,无法作为基准参考。

  C语言预处理器可剔除代码注释,但同时也会剔除#if 0...#endif之类的无效语句,不满足要求。于是,作者用UEStudio打开源文件,进入【搜索(Search)】|【替换(Replace)】页,选择Unix正则表达式引擎,用^\s*/\*.*\*/匹配单行注释(/*abc*/)并替换为空字符,用^\s*//.*$匹配单行注释(//abc)并替换为空字符。然后,查找并手工删除跨行注释及其他未匹配到的单行注释。最后,选择UltraEdit正则表达式引擎,用%[ ^t]++^p匹配空行并替换为空字符,即可删除所有空行。注意,UEStudio帮助中提供的正则表达式^p$一次只能删除一个空行。

  按上述方式处理两个大型文件后,初步发现BCLineCounter.py关于有效代码行数的统计是正确的。然而,这种半人工处理方式太过低效,因此作者想到让两个脚本处理相同的文件,并输出有效代码行或纯注释行的内容,将其通过Araxis Merge对比。该工具会高亮差异行,且人工检查很容易鉴别正误。此处,作者假定对于给定文件的给定类型行数,BCLineCounter.py和CLineCounter.py必有一者统计正确(可作基准)。当然,也有可能两者均有误差。因此,若求保险,也可同时输出类型和行内容,再行对比。

  综合检查结果发现,BCLineCounter.py较CLineCounter.py更为健壮。这是因为,模式匹配需要处理的场景繁多,极易疏漏。例如,CLineCounter.py无法正确处理下面的代码片段:

  void test(){

  /*/multiline,

  comment */

  int a = 1/2; //comment

  //* Assign a value

  }

  读者若有兴趣,可修改和调试CLineCounter.py里的正则表达式,使该脚本高效而健壮。

  以上内容为大家介绍了Python性能分析,希望对大家有所帮助,如果想要了解更多Python相关知识,请关注多测师。https://www.e70w.com/xwzx/


返回列表
在线客服
联系方式

热线电话

17727591462

上班时间

周一到周五

二维码
线