• Python实现C代码统计工具(三)


    Python实现C代码统计工具(三)

    标签: Python 性能分析 exe


    声明

    本文将对《Python实现C代码统计工具(二)》中的C代码统计工具进行性能分析,并制作Windows系统下可用的exe文件。本文所述的方法也适用于其他Python脚本。

    一. 性能分析

    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:Python27lib)
    #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 <string>:1(<module>)
            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:Python27libidlelibPyShell.py:1343(write)
            2    0.000    0.000    0.009    0.005 C:Python27libidlelib
    pc.py:591(__call__)
            2    0.000    0.000    0.009    0.005 C:Python27libidlelib
    pc.py:208(remotecall)
            2    0.000    0.000    0.009    0.004 C:Python27libidlelib
    pc.py:238(asyncreturn)
            2    0.000    0.000    0.009    0.004 C:Python27libidlelib
    pc.py:279(getresponse)
            2    0.000    0.000    0.009    0.004 C:Python27libidlelib
    pc.py:295(_getresponse)
            2    0.000    0.000    0.009    0.004 C:Python27lib	hreading.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:Python27lib
    e.py:138(match)
            1    0.000    0.000    0.002    0.002 C:Python27lib
    e.py:230(_compile)
    

    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            <Total:82 Code Files>
             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 + '
    ' #添加一个字符防止iChar+1时越界
        iChar, isLineComment = 0, False
        while iChar < lineLen:
            #行结束符(Windows:
    ; Mac:
    ; Unix:
    )
            if line[iChar] == '
    ' or line[iChar] == '
    ':
                break
            elif line[iChar] == ' ' or line[iChar] == '	':   #空白字符
                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"(即通用换行模式),该模式会将行结束符 和 替换为 。

    基于新的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            <Total:82 Code Files>
             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(<module>)
          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(<module>)
            1    0.002    0.002    3.926    3.926 BCLineCounter.py:6(<module>)
    

    可见,性能并不如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里的正则表达式,使该脚本高效而健壮。

    二. 制作exe

    py2exe可将Python脚本转换为Windows可执行程序(*.exe),以便在未安装Python环境的windows主机上独立运行该程序。

    登录SourceForge,可下载并安装与当前Python环境对应的py2exe版本。安装后,在Libsite-packagespy2exesamples目录下可找到多个官方使用示例。

    在Python交互窗口中运行如下命令,可查看py2exe的帮助信息:

    >>> import py2exe
    >>> help(py2exe)
    Help on package py2exe:
    ... ... ... ... ...
    

    若使用简单的setup.py打包文件,即setup(console=['CLineCounter.py']),则执行打包命令python mysetup.py py2exe后将在当前目录下创建build(1.28M)和dist(4.96M)两个子目录。其中,dist子目录包含CLineCounter.exe、python27.dll、w9xpopen.exe、library.zip及若干.pyd文件。通常,dist子目录的文件需要一起发布。但w9xpopen.exe用于Windows98系统,可以删除。

    显然,发布一个文件包不如发布单个可执行程序。同时,文件包的体积显得过大(CLineCounter.py只有不到7K)。因此,需要压缩体积并制作单一的exe。

    要压缩文件包体积,首先通过py2exe输出日志分析其打包细节。为缩短篇幅,以下给出裁剪过的py2exe输出日志(行尾带省略号则表示该行后面省略若干行):

    running py2exe
    creating E:PyTestuild
    creating E:PyTestuilddist.win32 ...
    creating E:PyTestdist ...
    byte-compiling C:Python27libdifflib.py to difflib.pyc ...
    byte-compiling C:Python27libdoctest.py to doctest.pyc ...
    byte-compiling C:Python27libencodingsascii.py to encodingsasc
    byte-compiling C:Python27libencodingscp1252.py to encodingscp1252.pyc ...
    byte-compiling C:Python27libencodingsgbk.py to encodingsgbk.pyc ...
    byte-compiling C:Python27libencodingsiso2022_jp.py to encodingsiso2022_jp.pyc ...
    byte-compiling C:Python27libencodingsiso8859_9.py to encodingsiso8859_9.pyc ...
    byte-compiling C:Python27libencodingsmac_arabic.py to encodingsmac_arabic.pyc ...
    byte-compiling C:Python27libencodingsshift_jis.py to encodingsshift_jis.pyc ...
    byte-compiling C:Python27libencodingsutf_8.py to encodingsutf_8.pyc ...
    byte-compiling C:Python27liboptparse.py to optparse.pyc ...
    byte-compiling C:Python27libpdb.py to pdb.pyc
    byte-compiling C:Python27libpickle.py to pickle.pyc ...
    byte-compiling C:Python27lib
    e.py to re.pyc ...
    byte-compiling C:Python27lib	hreading.py to threading.pyc ...
    byte-compiling C:Python27libunittestcase.py to unittestcase.pyc ...
    *** copy extensions ***
    copying C:Python27DLLs\_hashlib.pyd -> E:PyTestdist ...
    

    可见,很多不需要的模块也被打包,如日文编码文件和unittest等。此时,可备份Libencodings目录,然后手动删除原目录下不需要的语言文件。其他模块则可通过打包文件excludes选项进行排除。

    综上,作者采用以下打包文件(命名为PackOneExe.py):

    #!/usr/bin/python
    #coding=utf-8
    
    from distutils.core import setup
    import py2exe, sys, shutil
    
    #若未提供命令行参数,则以静默模式创建可执行文件(exe)
    if len(sys.argv) == 1:
        sys.argv.append('py2exe')
        sys.argv.append('-q')
    
    TARGET_PY = 'CLineCounter.py'
    EXE_DIR = TARGET_PY[:-3] + '_exe'
    #includes = ['encodings', 'encodings_trim.*']
    includes = ['encodings', 'encodings.gbk', 'encodings.utf_8']
    excludes = ['unittest', 'doctest', 'optparse', 'difflib',
                'pdb', 'threading', 'subprocess', 'pickle']
    dll_excludes = ['msvcp90.dll', 'mswsock.dll', 'powrprof.dll', 'w9xpopen.exe']
    
    setup(
        options = {'py2exe': {'compressed': 1,  #压缩
                              'optimize': 2,    #优化级别
                              'ascii': 1,       #不自动包含encodings和codecs
                              'includes': includes, #待包含模块名的列表
                              'excludes': excludes, #不予包含的模块名列表
                              'dist_dir': EXE_DIR,  #存放最终发布文件的目录
                              'bundle_files': 1 #所有文件打包为一个exe文件
                             }},
          zipfile = None,       #不生成library.zip文件
          console = [TARGET_PY] #待转换为控制台exe的脚本文件列表
    )
    
    shutil.rmtree('build')
    

    其中,options的bundle_files项指定将pyd和dll文件打包到单个exe文件中。此外,setup使用zipfile=None禁止创建library.zip。注意,此处假定C代码统计工具仅涉及中英文字符编码,因此仅包含gbk和utf-8两个编码文件。若为保险起见,可包含精简过的encodings目录(如注释所示)。options的excludes项对应一些未用到且体积较大的模块文件。

    PackOneExe.py对应的py2exe输出日志局部如下:

    byte-compiling C:Python27libdummy_thread.py to dummy_thread.pyo
    byte-compiling C:Python27libencodings\__init__.py to encodings\__init__.pyo
    byte-compiling C:Python27libencodingsaliases.py to encodingsaliases.pyo
    byte-compiling C:Python27libencodingsgbk.py to encodingsgbk.pyo
    byte-compiling C:Python27libencodingsutf_8.py to encodingsutf_8.pyo
    byte-compiling C:Python27libfunctools.py to functools.pyo
    

    将打包文件PackOneExe.py和主程序文件CLineCounter.py放在同一目录下,运行如下命令:

    E:PyTest>python PackOneExe.py py2exe > out.txt #或
    E:PyTest>PackOneExe.py > out.txt
    

    则在CLineCounter_exe子目录下生成2.68M的CLineCounter.exe文件。可见,文件体积相比先前的文件包有显著的缩减。

    接着,使用UPX工具进一步地压缩CLineCounter.exe文件。UPX是个通用的可执行文件压缩器,具有免费、压缩率高(效果优于zip/gzip)、解压速度快、支持多种可执行文件格式等优点,是压缩可执行文件时的首选工具。

    UPX以控制台命令行方式操作,可通过-h选项查看详细帮助。压缩命令如下:

    E:PyTest>upx -9 -f CLineCounter_exeCLineCounter.exe #或
    E:PyTest>upx -9 -f CLineCounter_exeCLineCounter.exe -o CLC.exe
    

    前者会用压缩文件覆盖原文件,后者保留原文件而另外生成压缩文件(CLC.exe)。注意,-n(1~9)选项指示压缩模式,数值越大压缩率越高,但压缩和解压速度越慢(影响加载速度)。--best压缩率最高,速度也最慢,不建议用于大文件。

    作者使用UPX 3.91w版本,可将2.68M的CLineCounter.exe文件压缩至1.22M。经检验运行正常。

  • 相关阅读:
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
    jQuery火箭图标返回顶部代码
  • 原文地址:https://www.cnblogs.com/clover-toeic/p/5524071.html
Copyright © 2020-2023  润新知