课件

2.5 代码性能分析.pdf

效率是程序员之间永恒的话题,能够编写更快更好的代码,是我们每一个程序员不懈的追求,但实际做起来,效率优化并不是一件很容易的事,它往往涉及到多方面的因素,这节课将结合一个实例,介绍有关代码性能分析和优化方面的内容。

代码性能优化

image.png
代码优化能够让程序运行的更快,是指在不改变程序运行结果的情况下,使得程序的运行效率更高。根据二八原则,实现程序的重构、优化、扩展以及文档相关的事情,通常是要消耗80%的工作量。程序的性能一般涉及到时间空间两个方面,优化也就包括减少代码的体积提高代码的运行效率这两项内容。

image.png
代码性能优化必须要在满足正确性、可靠性、健壮性、可读性这些质量因素的前提下进行。优化时,要注意从程序的全局效率来考虑,不能单纯考虑局部,而且要先找出限制效率的瓶颈,然后再对症下药。由于数据结构和算法对程序效率起到关键的作用,因此优化的首要点是数据结构和算法,其次才是执行代码。需要说明的是时间效率和空间效率往往是相互对立的,优化的时候应当分析哪一个因素更重要,然后再作出适当的折中。

image.png
程序性能应该在编程一开始就要考虑进去,不要期待通过事后的调整来进行提升。另外任何优化都不能破坏代码的正确性,一般来说代码优化过程包括以下步骤:

  • 首先要证明代码的性能确实需要提升;
  • 然后要找出优化的关键部分;
  • 再通过测试找出影响代码性能的瓶颈所在;
  • 对代码进行优化;
  • 最后还要对优化后的代码再进行测试,以确定优化的效果;

这里需要强调的是,测试数据的选择必须能够代表实际的使用情况。另外永远不要在没有执行前后性能评估的情况下,尝试对代码进行优化。

案例分析

现在我们通过一个程序示例,来详细说明代码优化的步骤和方法。

这个程序要求读入一个文本文件,统计在该文件中每个英文单词出现的频率,并输出频率最高的100个单词,其中单词指的是连续的若干个小写英文字母。

在这里我们给出了,几个单词的示例,像最后这一行它是被逗号、句号、空格分割成了四个单词:
image.png

这是用Python实现的一个程序代码例子,我们先介绍一下它的基本结构:
image.png

  • 文件头部是关于这个程序文件的注释,包括代码格式、作者信息等;
  • 文件头的下面是程序中需要的各种包;
  • 包下面是实现程序主要功能的分词函数;
  • 最后两行是主程序的入口,相当于C++的main函数;

现在来分析一下分词函数中可能影响性能的部分:
image.png

  • 第一是文件读入部分,文件的IO通常是速度比较慢的;
  • 第二是分割单词部分,这里使用了正则表达式来实现;
  • 第三是词频统计部分,我们用词典类型进行判重操作;
  • 最后是排序部分,这里使用了Python内置的排序算法来实现;

那影响程序性能的主要是哪一部分呢?显然瞎猜是不行的,我们需要使用性能分析工具来获得实际的测试数据,再来确定真正的性能瓶颈。

性能测试工具

Profile是Python语言内置的性能分析工具,它能够有效地描述程序运行时的性能状况,提供各种统计数据,帮助程序员找出程序的性能瓶颈。

image.png
Profile的使用非常简单,像图上给出的这个求阶乘函数的例子,先在文件中引入Profle模块,再以程序的入口函数名为参数,来调用Profile.run这个函数,接下来就可以使用Profile进行性能分析。

image.png
上图显示的是Profile对刚才那个阶乘函数生成的测试报告结果:

  • 上面是程序的输出结果;
  • 中间是程序的执行时间,由于程序非常小,所以几乎没有什么耗时;
  • 下面是详细的函数性能数据报表,它包括了函数被调用的次数、总的运行时间、运行一次的平均时间及函数所在的文件名、行号、函数名等。

图中划横线部分显示了print这个函数被调用了10次,及其他的一些运行时间等。需要说明的是,tottime是除去了函数中调用其他函数的总运行时间,cumtime是包括了调用其他函数的总运行时间,两者是有区别的。

性能分析

现在我们来看一下前面的词频统计程序的性能数据报表:
image.png

  • 程序的总耗时是4.562秒;
  • 其中keys函数被执行了50万次,耗时1.141秒;
  • 输出几乎没有耗时;
  • 输入耗时0.094秒;
  • 排序函数的耗时是1.203秒;
  • 分词函数耗时0.312秒;

这些测试数据告诉我们,keys函数和排序函数的耗时是比较多的,但是排序函数是Python内置的函数不容易进行优化,因此我们决定优化keys函数。

需要说明的是,虽然性能分析工具指明了程序各个部分的耗时,但并不意味着我们改进效率一定要优先改进耗时最多的部分。虽然改进耗时最多的部分往往效果是明显的,但有的时候可能很难改进,在实际的项目中,我们需要在改进得到的效果和改进要投入的精力之间进行平衡,来优先完成有能力做而且效果又比较明显的性能优化。

那么我们先分析一下,为什么 keys() 函数的调用复杂度过高?
image.png
经过查阅资料,我们发现keys函数每调用一次,系统就会生成一个新的词典迭代器,如果这个过程重复50万次,那么效率是不可以想象的。所以我们提出了一种优化的方案,就是使用in操作符直接代替keys,然后这样就可以避免每一次生成新的迭代器。

这是改进后的程序代码:
image.png
框选部分就是修改后的代码,对修改后的代码再进行测试,结果发现程序的总执行时间降低到2.250s。

image.png
经过简单的优化,我们把程序的执行时间从之前的4.562秒降低到了2.250秒,差不多缩短了一半,效果还是比较明显的,这个例子告诉我们:

  • 性能优化的关键是如何发现问题和解决问题;
  • 在这个过程中,有效的测试是不可缺少的,我们通过测试找出真正的瓶颈,并且分析优化的结果;
  • 需要注意的是,一定要避免不必要的优化和不成熟的优化,因为不成熟的优化有可能引来错误;

Python 代码性能优化建议

最后我们针对Python代码的性能优化,给出一些建议,仅供大家参考。

image.png
image.png
首先是改进算法,并选择合适的数据结构
1、算法的好坏对程序的性能是非常关键的,因此首先要改进算法的效率。
2、在这里我们列出了算法的时间复杂度的排序,想必大家在算法课程的学习中已经有了基本的了解。
3、对于成员的查找访问等操作,字典要比列表更快。因为Python字典中使用了哈希表它的查找复杂度是O(1),而列表实际上就是一个数组,查找需要遍历整个列表,复杂度是O (n)。所以在需要多数据成员进行频繁的查找或者访问的时候,使用字典更好一些
4、集合的并、交、差的操作比列表的迭代要快,涉及到求列表的并、交、差问题,可以转化为集合来操作。

循环优化的基本原则:是尽量减少循环中的计算量,有多重循环的时候尽量将内层的计算提到上一层。

字符串的优化:对于字符串的优化,由于Python的字符串对象是不可改变的,字符串的连接尽量使用join函数,当对字符串可以使用正则表达式或者内置函数处理时,选择内置函数。

使用列表解析和生成器表达式:列表解析要比在循环中重新建一个新的列表更为高效,因此可以利用这个特性来提高代码的运行效率。