Python 代码性能优化技巧
选择了脚本语言就要忍受其速度,这句话在某种程度上说明了 python 作为脚本的一个不足之处,那就是执行效率和性能不够理想,特别是在 performance 较差的机器上,因此有必要进行一定的代码优化来提高程序的执行效率。
Python为什么性能差?
1、python是动态语言
一个变量所指向对象的类型在运行时才确定,编译器做不了任何预测,也就无从优化。举一个简单的例子: r = a + b。 a和b相加,但a和b的类型在运行时才知道,对于加法操作,不同的类型有不同的处理,所以每次运行的时候都会去判断a和b的类型,然后执行对应的操作。而在静态语言如C++中,编译的时候就确定了运行时的代码。
另外一个例子是属性查找,关于具体的查找顺序在《python属性查找》中有详细介绍。简而言之,访问对象的某个属性是一个非常复杂的过程,而且通过同一个变量访问到的python对象还都可能不一样(参见Lazy property的例子)。而在C语言中,访问属性用对象的地址加上属性的偏移就可以了。
2、python是解释执行
Python不支持JIT(just in time compiler)。虽然大名鼎鼎的google曾经尝试Unladen Swallow 这个项目,但最终也折了。
3、python中一切都是对象
每个对象都需要维护引用计数,增加了额外的工作。
4、python GIL
GIL是Python最为诟病的一点,因为GIL,python中的多线程并不能真正的并发。如果是在IO bound的业务场景,这个问题并不大,但是在CPU BOUND的场景,这就很致命了。所以笔者在工作中使用python多线程的情况并不多,一般都是使用多进程(pre fork),或者在加上协程。即使在单线程,GIL也会带来很大的性能影响,因为python每执行100个opcode(默认,可以通过sys.setcheckinterval()设置)就会尝试线程的切换,具体的源代码在ceval.c::PyEval_EvalFrameEx。
5、垃圾回收
这个可能是所有具有垃圾回收的编程语言的通病。python采用标记和分代的垃圾回收策略,每次垃圾回收的时候都会中断正在执行的程序,造成所谓的顿卡。infoq上有一篇文章,提到禁用Python的GC机制后,Instagram性能提升了10%。
Python代码优化常用技巧
- 减小代码体积
- 提高代码的运行效率
一、改进算法,选择合适的数据结构(更好的选择已经用蓝色字标注出来了)
在算法的时间复杂度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
当然选择更合理的算法是最好的优化手段,但是在算法没有办法更加合理化的时候我们就要选择更好的数据结构。
1、字典(dictionary)与列表(list)
Python 字典中使用了 hash table,因此查找操作的复杂度为 O(1),而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此对成员的查找访问等操作字典要比 list 更快。
使用字典会比使用列表效率大概提高一半。
因此在需要多数据成员进行频繁的查找或者访问的时候,使用 dict 而不是 list 是一个较好的选择。
2、集合(set)和列表(list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。(附表一)
表 1. set 常见用法
语法 | 操作 | 说明 |
set(list1) | set(list2) | union | 包含 list1 和 list2 所有数据的新集合 |
set(list1) & set(list2) | intersection | 包含 list1 和 list2 中共同元素的新集合 |
set(list1) - set(list2) | difference | 在 list1 中出现但不在 list2 中出现的元素的集合 |
3、对循环的优化
对循环的优化所遵循的原则是尽量减少循环过程中的计算量,有多重循环的尽量将内层的计算提到上一层。
4、充分利用Lazy if-evaluation 的特性
python 中条件表达式是 lazy evaluation 的,也就是说如果存在条件表达式 if x and y,在 x 为 false 的情况下 y 表达式的值将不再计算。
所以在保证不改变运行结果的前提下,尽量减少IF里面的条件来提高程序的效率
5、字符串的优化
- 在字符串连接的使用尽量使用 join() 而不是 +;
- 当对字符串可以使用正则表达式或者内置函数来处理的时候,选择内置函数。如 str.isalpha(),str.isdigit(),str.startswith(('x', 'yz')),str.endswith(('x', 'yz'));
- 对字符进行格式化比直接串联读取要快,因此要使用
1 out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)
而不是
1 out = "<html>" + head + prologue + query + tail + "</html>"
6、使用列表解析(list comprehension)和生成器表达式(generator expression)
列表解析要比在循环中重新构建一个新的 list 更为高效,因此我们可以利用这一特性来提高运行的效率。
1 for i in range (1000000): 2 for w in list: 3 total.append(w)
使用列表解析:
1 for i in range (1000000): 2 a = [w for w in list]
7、其他优化技巧
- 如果需要交换两个变量的值使用 a,b=b,a 而不是借助中间变量 t=a;a=b;b=t;
- 在循环的时候使用 xrange 而不是 range;使用 xrange 可以节省大量的系统内存,因为 xrange() 在序列中每次调用只产生一个整数元素。而 range() 將直接返回完整的元素列表,用于循环时会有不必要的开销;
- 使用局部变量,避免"global" 关键字。python 访问局部变量会比全局变量要快得多;
- if done is not None 比语句 if done != None 更快;
- 在耗时较多的循环中,可以把函数的调用改为内联的方式;
- 使用级联比较 "x < y < z" 而不是 "x < y and y < z";
- while 1 要比 while True 更快(当然后者的可读性更好);
- build in 函数通常较快,add(a,b) 要优于 a+b。
二、定位程序性能瓶颈
使用 profile 进行性能分析
其中 Profiler 是 python 自带的一组程序,能够描述程序运行时候的性能,并提供各种统计帮助用户定位程序的性能瓶颈。
profile 的使用非常简单,只需要在使用之前进行 import 即可。具体实例如下:
1 import profile 2 def profileTest(): 3 Total =1; 4 for i in range(10): 5 Total=Total*(i+1) 6 print Total 7 return Total 8 if __name__ == "__main__": 9 profile.run("profileTest()")
程序的运行结果如下:
其中输出每列的具体解释如下:
- ncalls:表示函数调用的次数;
- tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间;
- percall:(第一个 percall)等于 tottime/ncalls;
- cumtime:表示该函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间;
- percall:(第二个 percall)即函数运行一次的平均时间,等于 cumtime/ncalls;
- filename:lineno(function):每个函数调用的具体信息;
三、Python性能优化工具
• Python 性能优化除了改进算法,选用合适的数据结构之外,还有几种关键的技术,比如将关键 python 代码部分重写成 C 扩展模块,或者选用在性能上更为优化的解释器等,这些在本文中统称为优化工具。python 有很多自带的优化工具,如 Pypy,Cython,Pyrex 等,这些优化工具各有千秋,本节选择几种进行介绍。
1、Pypy
PyPy 表示 "用 Python 实现的 Python",但实际上它是使用一个称为 RPython 的 Python 子集实现的,能够将 Python 代码转成 C, .NET, Java 等语言和平台的代码。PyPy 集成了一种即时 (JIT) 编译器。和许多编译器,解释器不同,它不关心 Python 代码的词法分析和语法树。 因为它是用 Python 语言写的,所以它直接利用 Python 语言的 Code Object.。 Code Object 是 Python 字节码的表示,也就是说, PyPy 直接分析 Python 代码所对应的字节码 ,,这些字节码即不是以字符形式也不是以某种二进制格式保存在文件中, 而在 Python 运行环境中。目前版本是 1.8. 支持不同的平台安装,windows 上安装 Pypy 需要先下载 ,然后解压到相关的目录,并将解压后的路径添加到环境变量 path 中即可。
接下来我们来测试一下使用同一个程序python解释器和pypy解释器的编译时间为多少?到底有没有提升速度?
1 import time 2 3 t = time.time() 4 for i in range(10 ** 8): 5 continue 6 print(time.time() - t)
我们可以看到这是python解释器的编译时间:
下面这是pypy解释器的编译时间:
从上面对比我们发现python解释器的编译时间为:5.84,
而pypy解释器的编译时间为0.32,
从编译时间上我们可以看出速度提升了将近22倍!!
2、Cython
Cython 是用 python 实现的一种语言,可以用来写 python 扩展,用它写出来的库都可以通过 import 来载入,性能上比 python 的快。cython 里可以载入 python 扩展 ( 比如 import math),也可以载入 c 的库的头文件 ( 比如 :cdef extern from "math.h"),另外也可以用它来写 python 代码。将关键部分重写成 C 扩展模块
Linux Cpython 的安装可以参考文档
Cython 代码与 python 不同,必须先编译,编译一般需要经过两个阶段,将 pyx 文件编译为 .c 文件,再将 .c 文件编译为 .so 文件。编译有多种方法:
- 通过命令行编译
- 使用 distutils 编译
下面来进行一个简单的性能比较:
1 from time import time 2 def test(int n): 3 cdef int a =0 4 cdef int i 5 for i in xrange(n): 6 a+= i 7 return a 8 9 t = time() 10 test(10000000) 11 print "total run time:" 12 print time()-t
测试结果:
[GCC 4.0.2 20051125 (Red Hat 4.0.2-8)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import pyximport; pyximport.install() >>> import ctest total run time: 0.00714015960693
使用python测试结果:
通过清楚地对比可以发现使用 Cython 的速度提升了100多倍。