python标准库提供两个代码性能分析相关的模块,即timeit和cProfile/profile。前者更适合测试简短的代码片段,后者则可分析代码片段乃至整体模块中各个函数的调用次数、运行耗时等信息。
cProfile是profile的C版本,开销更小。基于cProfile模块,可方便地评估程序性能瓶颈(bottleneck),借以发现程序中值得优化的短板。
根据粒度不同,可将cProfile使用场景分为三类。
1.1分析单条语句
importcProfile,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性能分析结果进行排序并输出。运行结果如下:
TueMay2413:56:072016prfRes
195functioncalls(190primitivecalls)in0.001seconds
Orderedby:cumulativetime
Listreducedfrom33to5duetorestriction<5>
ncallstottimepercallcumtimepercallfilename:lineno(function)
10.0000.0000.0010.001:1()
10.0000.0000.0010.001re.py:192(compile)
10.0000.0000.0010.001re.py:230(_compile)
10.0000.0000.0010.001sre_compile.py:567(compile)
10.0000.0000.0000.000sre_compile.py:552(_code)
其中,tottime表示某函数的总计运行时间(不含该函数内调用的子函数运行时间),cumtime表示某函数及其调用的子函数的累积运行时间。
1.2分析代码片段
pr=cProfile.Profile()
pr.enable()#以下为待分析代码段
regMatch=re.match('^([^/]*)/(/|\*)+(.*)$','//*suspicious')
printregMatch.groups()
pr.disable()#以上为待分析代码段
s=cStringIO.StringIO()
pstats.Stats(pr,stream=s).sort_stats('cumulative').print_stats(10)
prints.getvalue()
运行结果如下:
('','*','suspicious')
536functioncalls(512primitivecalls)in0.011seconds
Orderedby:cumulativetime
Listreducedfrom78to10duetorestriction<10>
ncallstottimepercallcumtimepercallfilename:lineno(function)
20.0000.0000.0090.005C:\Python27\lib\idlelib\PyShell.py:1343(write)
20.0000.0000.0090.005C:\Python27\lib\idlelib\rpc.py:591(__call__)
20.0000.0000.0090.005C:\Python27\lib\idlelib\rpc.py:208(remotecall)
20.0000.0000.0090.004C:\Python27\lib\idlelib\rpc.py:238(asyncreturn)
20.0000.0000.0090.004C:\Python27\lib\idlelib\rpc.py:279(getresponse)
20.0000.0000.0090.004C:\Python27\lib\idlelib\rpc.py:295(_getresponse)
20.0000.0000.0090.004C:\Python27\lib\threading.py:309(wait)
80.0090.0010.0090.001{method'acquire'of'thread.lock'objects}
10.0000.0000.0020.002C:\Python27\lib\re.py:138(match)
10.0000.0000.0020.002C:\Python27\lib\re.py:230(_compile)
1.3分析整个模块
使用命令行,调用cProfile脚本分析其他脚本文件。命令格式为:
python-mcProfile[-ooutput_file][-ssort_order]myscript.py
注意,-o和-s选项不可同时使用。
以C代码统计工具为例,运行如下命令:
E:\PyTest>python-mcProfile-stottimeCLineCounter.pysource-d-b>out.txt
截取out.txt文件部分内容如下:
250316245433620.25xtm_mgr.c
1408729374932093169380.26
762068functioncalls(762004primitivecalls)in2.967seconds
Orderedby:internaltime
ncallstottimepercallcumtimepercallfilename:lineno(function)
820.9850.0122.8690.035CLineCounter.py:11(CalcLines)
1176400.6120.0001.3150.000re.py:138(match)
1176500.3810.0000.3810.000{method'match'of'_sre.SRE_Pattern'objects}
1176550.3190.0000.3240.000re.py:230(_compile)
1380500.1980.0000.1980.000{method'isspace'of'str'objects}
1058230.1650.0000.1650.000{method'strip'of'str'objects}
123156/1231410.1540.0000.1540.000{len}
378870.0550.0000.0550.000{method'group'of'_sre.SRE_Match'objects}
820.0410.0000.0410.000{method'readlines'of'file'objects}
820.0160.0000.0160.000{open}
10.0040.0042.9502.950CLineCounter.py:154(CountDir)
由tottime可见,此处的性能瓶颈在于CalcLines()函数和其中的正则表达式处理。而isspace()和strip()方法及len()函数因调用次数较多,总的耗时也颇为可观。
作为对比,以下给出一种未使用正则表达式的统计实现:
defCalcLines(line,isBlockComment):
lineType,lineLen=0,len(line)
line=line+'\n'#添加一个字符防止iChar+1时越界
iChar,isLineComment=0,False
whileiChar #行结束符(Windows:\r\n;Mac:\r;Unix:\n) ifline[iChar]=='\r'orline[iChar]=='\n': break elifline[iChar]==''orline[iChar]=='\t':#空白字符 iChar+=1;continue elifline[iChar]=='/'andline[iChar+1]=='/':#行注释 isLineComment=True lineType|=2;iChar+=1#跳过'/' elifline[iChar]=='/'andline[iChar+1]=='*':#块注释开始符 isBlockComment[0]=True lineType|=2;iChar+=1 elifline[iChar]=='*'andline[iChar+1]=='/':#块注释结束符 isBlockComment[0]=False lineType|=2;iChar+=1 else: ifisLineCommentorisBlockComment[0]: lineType|=2 else: lineType|=1 iChar+=1 returnlineType#Bitmap:0空行,1代码,2注释,3代码和注释 在CalcLines()函数中。参数line为当前文件行字符串,参数isBlockComment指示当前行是否位于块注释内。该函数直接分析句法,而非模式匹配。注意,行结束符可能因操作系统而异,因此应区分CR(回车)和LF(换行)符。此外,也可在读取文件时采用"rU"(即通用换行模式),该模式会将行结束符\r\n和\r替换为\n。 基于新的CalcLines()函数,CountFileLines()函数需作如下修改: defCountFileLines(filePath,isRawReport=True,isShortName=False): fileExt=os.path.splitext(filePath) iffileExt[1]!='.c'andfileExt[1]!='.h': return isBlockComment=[False]#或定义为全局变量,以保存上次值 lineCountInfo=[0]*4#[代码总行数,代码行数,注释行数,空白行数] withopen(filePath,'r')asfile: forlineinfile: lineType=CalcLines(line,isBlockComment) lineCountInfo[0]+=1 iflineType==0:lineCountInfo[3]+=1 eliflineType==1:lineCountInfo[1]+=1 eliflineType==2:lineCountInfo[2]+=1 eliflineType==3:lineCountInfo[1]+=1;lineCountInfo[2]+=1 else: assertFalse,'UnexpectedlineType:%d(0~3)!'%lineType ifisRawReport: globalrawCountInfo rawCountInfo[:-1]=[x+yforx,yinzip(rawCountInfo[:-1],lineCountInfo)] rawCountInfo[-1]+=1 elifisShortName: detailCountInfo.append([os.path.basename(filePath),lineCountInfo]) else: detailCountInfo.append([filePath,lineCountInfo]) 将这种统计实现命名为BCLineCounter.py。通过cProfile命令分析其性能,截取out.txt文件部分内容如下: 250316245433620.25xtm_mgr.c 1408729373632106169380.26 286013functioncalls(285979primitivecalls)in3.926seconds Orderedby:internaltime ncallstottimepercallcumtimepercallfilename:lineno(function) 1408723.3340.0003.4750.000BCLineCounter.py:15(CalcLines) 830.4090.0053.9030.047BCLineCounter.py:45(CountFileLines) 141593/1415850.1420.0000.1420.000{len} 820.0140.0000.0140.000{open} 10.0040.0040.0040.004collections.py:1() 4160.0030.0000.0040.000ntpath.py:96(splitdrive) 840.0020.0000.0020.000{nt._isdir} 10.0020.0020.0070.007argparse.py:62() 10.0020.0023.9263.926BCLineCounter.py:6() 可见,性能并不如CLineCounter.py。因此,使用标准库(如re)提供的函数或方法,不失为明智的选择。 此外,对比BCLineCounter.py和CLineCounter.py的详细行数报告可知,两者的统计结果存在细微差异(正负误差不超过5行)。差异主要体现在有效代码行和纯注释行统计上,因为总行数和空白行数通常不会出现统计误差。那么,哪种实现更可靠呢? 作者首先想到挑选存在统计差异的文件,人工或半人工地删除纯注释行和空白行,从而得到精确的有效代码行数。之所以不编写脚本自动删除上述类型的文件行,是因为作者对于注释行的解析已经存在误差,无法作为基准参考。 C语言预处理器可剔除代码注释,但同时也会剔除#if0...#endif之类的无效语句,不满足要求。于是,作者用UEStudio打开源文件,进入【搜索(Search)】|【替换(Replace)】页,选择Unix正则表达式引擎,用^\s*/\*.*\*/匹配单行注释(/*abc*/)并替换为空字符,用^\s*//.*$匹配单行注释(//abc)并替换为空字符。然后,查找并手工删除跨行注释及其他未匹配到的单行注释。最后,选择UltraEdit正则表达式引擎,用%[^t]++^p匹配空行并替换为空字符,即可删除所有空行。注意,UEStudio帮助中提供的正则表达式^p$一次只能删除一个空行。 按上述方式处理两个大型文件后,初步发现BCLineCounter.py关于有效代码行数的统计是正确的。然而,这种半人工处理方式太过低效,因此作者想到让两个脚本处理相同的文件,并输出有效代码行或纯注释行的内容,将其通过AraxisMerge对比。该工具会高亮差异行,且人工检查很容易鉴别正误。此处,作者假定对于给定文件的给定类型行数,BCLineCounter.py和CLineCounter.py必有一者统计正确(可作基准)。当然,也有可能两者均有误差。因此,若求保险,也可同时输出类型和行内容,再行对比。 综合检查结果发现,BCLineCounter.py较CLineCounter.py更为健壮。这是因为,模式匹配需要处理的场景繁多,极易疏漏。例如,CLineCounter.py无法正确处理下面的代码片段: voidtest(){ /*/multiline, comment*/ inta=1/2;//comment //*Assignavalue } 读者若有兴趣,可修改和调试CLineCounter.py里的正则表达式,使该脚本高效而健壮。 以上内容为大家介绍了Python性能分析,希望对大家有所帮助,如果想要了解更多Python相关知识,请关注IT培训机构:千锋教育。