http://blog.csdn.net/pipisorry/article/details/39998317
python 作为脚本的一个不足之处,那就是执行效率和性能不够理想,特别是在 performance 较差的机器上,因此有必要进行一定的代码优化来提高python程序的执行效率。
Python 代码优化常见技巧
当你的程序运行地很慢的时候,你就会想去提升它的运行速度,但是你又不想去借用一些复杂方案的帮助,比如使用C扩展或是just-in-time(JIT)编译器。
那么这时候应该怎么办呢?要牢记性能优化的第一要义就是“不要为了优化而去优化,应该在我们开始写代码之前就想好应该怎样编写高性能的代码”。第二要义就是“优化一定要抓住重点,找到程序中最重要的地方去优化,而不要去优化那些不重要的部分”。通常来讲,你会发现你的程序在某些热点上花费了很多时间,比如内部数据的循环处理。一旦你发现了问题所在,你就可以对症下药,让你的程序更快地执行。
代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构、优化、扩展以及文档相关的事情通常需要消耗 80% 的工作量。优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率。
改进算法
一个良好的算法能够对性能起到关键作用,因此性能改进的首要点是对算法的改进。在算法的时间复杂度排序上依次是:
O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)
因此如果能够在时间复杂度上对算法进行一定的改进,对性能的提高不言而喻。
如:list和set查找某一个元素的时间复杂度分别是O(n)和O(1)。
并行编程
因为GIL的存在,Python很难充分利用多核CPU的优势。但是,可以通过内置的模块multiprocessing实现下面几种并行模式:
多进程:对于CPU密集型的程序,可以使用multiprocessing的Process,Pool等封装好的类,通过多进程的方式实现并行计算。但是因为进程中的通信成本比较大,对于进程之间需要大量数据交互的程序效率未必有大的提高。
多线程:对于IO密集型的程序,multiprocessing.dummy模块使用multiprocessing的接口封装threading,使得多线程编程也变得非常轻松(比如可以使用Pool的map接口,简洁高效)。
分布式:multiprocessing中的Managers类提供了可以在不同进程之共享数据的方式,可以在此基础上开发出分布式的程序。
不同的业务场景可以选择其中的一种或几种的组合实现程序性能的优化。
选择合适的数据结构
使用内置的容器
内置的数据结构,例如字符串(string),元组(tuple),列表(list),集合(set)以及字典(dict)都是用C语言实现的,正是因为采用了C来实现,所以它们的性能表现也很好。如果你倾向于使用你自己的数据结构作为替代的话(例如,链表,平衡树或是其他数据结构),想达到内置数据结构的速度的话是非常困难的。因此,你应该尽可能地使用内置的数据结构。字典 (dictionary) 与列表 (list)
python dict和set都是使用hash表来实现(类似c++11标准库中unordered_map),查找元素的时间复杂度是O(1)。而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此对成员的查找访问等操作字典要比 list 更快。
t = time.time()
list = ['a', 'b', 'is', 'python', 'jason', 'hello', 'hill', 'with', 'phone', 'test',
'dfdf', 'apple', 'pddf', 'ind', 'basic', 'none', 'baecr', 'var', 'bana', 'dd', 'wrd']
# list = dict.fromkeys(list, True)
print(list)
filter = []
for i in range(1):
for find in ['is', 'hat', 'new', 'list', 'old', '.']:
if find not in list:
filter.append(find)
print(filter)#['hat', 'new', 'list', 'old', '.']
print("total run time:")
print(time.time() - t)
如果去掉行 #list = dict.fromkeys(list,True) 的注释,将 list 转换为字典之后再运行,时效率大概提高了一大半。因此在需要多数据成员进行频繁的查找或者访问的时候,使用 dict 而不是 list 是一个较好的选择。
Python 字典(Dictionary) fromkeys() 函数用于创建一个新字典,以序列seq中元素做字典的键,value为字典所有键对应的初始值。
fromkeys()方法语法:dict.fromkeys(seq[, value]))
Note:dict效率高但占用的空间也多一些。
字典
谨慎地对待微优化(micro-optimization)的结果。例如,考虑下面两种创建字典结构的方式:
1
2
3
4
5
6
|
a
=
{
'name'
:
'AAPL'
,
'shares'
:
100
,
'price'
:
534.22
}
b
=
dict
(name
=
'AAPL'
, shares
=
100
, price
=
534.22
)
|
集合 (set) 与列表 (list)
set 的 union, intersection,difference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。
还有list和set查找某一个元素的时间复杂度分别是O(n)和O(1)。求 list 的交集:
from time import time t = time() lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44] listb=[2,4,6,9,23] intersection=[] for i in range (1000000): for a in lista: for b in listb: if a == b: intersection.append(a) print "total run time:" print time()-t
上述程序的运行时间大概为:
total run time: 38.4070000648
用 set 求交集
from time import time t = time() lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44] listb=[2,4,6,9,23] intersection=[] for i in range (1000000): list(set(lista)&set(listb)) print "total run time:" print time()-t
改为 set 后程序的运行时间缩减为 8.75,提高了 4 倍多,运行时间大大缩短。
例如使用列表解析时,if语句中有in list语句时
nonfs_list = [(u, v) for u, v in uv_meet_dict if u < v and (u, v) not in fs_list] # 性能差,运行奇慢,时间59.47
nonfs_list = [(u, v) for u, v in uv_meet_dict if u < v]
nonfs_list = list(set(nonfs_list) - set(fs_list))
或者改成dict的操作更快
d = dict.fromkeys(fs_list, True) #这个可别放入到列表解析中去了
nonfs_list = [(u, v) for u, v in uv_meet_dict if u < v and (u, v) not in d]
时间0.0181
使用 numpy
对于大数据,使用 numpy,它比标准的数据结构好很多。
减少冗余数据
如用上三角或下三角的方式去保存一个大的对称矩阵。在0元素占大多数的矩阵里使用稀疏矩阵表示。合理使用生成器(generator)和yield
%timeit -n 100 a = (i for i in range(100000))%timeit -n 100 b = [i for i in range(100000)]
100 loops, best of 3: 1.54 ms per loop
100 loops, best of 3: 4.56 ms per loop
使用()得到的是一个generator对象,所需要的内存空间与列表的大小无关,所以效率会高一些。在具体应用上,比如set(i for i in range(100000))会比set([i for i in range(100000)])快。
但是对于需要循环遍历的情况:
%timeit -n 10 for x in (i for i in range(100000)): pass
%timeit -n 10 for x in [i for i in range(100000)]: pass
10 loops, best of 3: 6.51 ms per loop
10 loops, best of 3: 5.54 ms per loop
后者的效率反而更高,但是如果循环里有break,用generator的好处是显而易见的。yield也是用于创建generator:
def yield_func(ls):
for i in ls:
yield i+1
def not_yield_func(ls):
return [i+1 for i in ls]
ls = range(1000000)
%timeit -n 10 for i in yield_func(ls):pass
%timeit -n 10 for i in not_yield_func(ls):pass
10 loops, best of 3: 63.8 ms per loop
10 loops, best of 3: 62.9 ms per loop
对于内存不是非常大的list,可以直接返回一个list,但是可读性yield更佳(人个喜好)。
python2.x内置generator功能的有xrange函数、itertools包等。
循环优化
循环性能
1. list comphrension > for loop > while
列表推导比循环遍历列表快,但 while loop 是最慢的,需要使用一个外部计数器。
2. 使用 Map ,Reduce 和 Filter 代替 for 循环(py2中)
2. while 1 比 while True 快(当然后者的可读性更好)
3. 在循环的时候使用 xrange 而不是 range;使用 xrange 可以节省大量的系统内存,因为 xrange() 在序列中每次调用只产生一个整数元素。而 range() 將直接返回完整的元素列表,用于循环时会有不必要的开销。在 python3 中 xrange 不再存在,里面 range 提供一个可以遍历任意长度的范围的 iterator。xrange 是 range 的 C 实现,着眼于有效的内存使用。(在 Python2.x 中这样做,因为 Python 3.x 中是默认的)
4. 列表和迭代器版本存在 - 迭代器是内存效率和可伸缩性的。使用 itertools
创建生成器以及尽可能使用 yeild,它们比正常的列表方式更快。
循环的优化
对循环的优化所遵循的原则是尽量减少循环过程中的计算量,有多重循环的尽量将内层的计算提到上一层。
为进行循环优化前(大概的运行时间约为 132.375)
from time import time t = time() lista = [1,2,3,4,5,6,7,8,9,10] listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01] for i in range (1000000): for a in range(len(lista)): for b in range(len(listb)): x=lista[a]+listb[b] print "total run time:" print time()-t
现在进行如下优化,将长度计算提到循环外,range 用 xrange 代替(python3就不用了,range就是xrange),同时将第三层的计算 lista[a] (也相当于一个计算)提到循环的第二层。
循环优化后
from time import time
t = time()
lista = [1,2,3,4,5,6,7,8,9,10]
listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01]
len1=len(lista)
len2=len(listb)
for i in range(1000000):
for a in range(len1):
temp=lista[a]
for b in range(len2):
x=temp+listb[b]
print("total run time:")
print(time()-t)
上述优化后的程序其运行时间缩短为 102.171999931。在清单 4 中 lista[a] 被计算的次数为 1000000*10*10,而在优化后的代码中被计算的次数为 1000000*10,计算次数大幅度缩短,因此性能有所提升。
充分利用 Lazy if-evaluation 的特性
python 中条件表达式是 lazy evaluation 的,也就是说如果存在条件表达式 if x and y,在 x 为 false 的情况下 y 表达式的值将不再计算。因此可以利用该特性在一定程度上提高程序效率。
利用 Lazy if-evaluation 的特性
from time import time t = time() abbreviations = ['cf.', 'e.g.', 'ex.', 'etc.', 'fig.', 'i.e.', 'Mr.', 'vs.'] for i in range (1000000): for w in ('Mr.', 'Hat', 'is', 'chasing', 'the', 'black', 'cat', '.'): if w in abbreviations: #if w[-1] == '.' and w in abbreviations: pass print "total run time:" print time()-t
在未进行优化之前程序的运行时间大概为 8.84,如果使用注释行代替第一个 if,运行的时间大概为 6.17。
优化包含多个判断表达式的顺序
对于and,应该把满足条件少的放在前面,对于or,把满足条件多的放在前面。如:全选复制放进笔记a = range(2000)
%timeit -n 100 [i for i in a if 10 < i < 20 or 1000 < i < 2000]
%timeit -n 100 [i for i in a if 1000 < i < 2000 or 100 < i < 20]
%timeit -n 100 [i for i in a if i % 2 == 0 and i > 1900]
%timeit -n 100 [i for i in a if i > 1900 and i % 2 == 0]
100 loops, best of 3: 287 µs per loop
100 loops, best of 3: 214 µs per loop
100 loops, best of 3: 128 µs per loop
100 loops, best of 3: 56.1 µs per loop
字符串的优化
python 中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的 copy 会在一定程度上影响 python 的性能。对字符串的优化也是改善性能的一个重要的方面,特别是在处理文本较多的情况下。
字符串的优化主要集中在以下几个方面:
在字符串连接的使用尽量使用 join() 而不是 +:在代码清单 7 中使用 + 进行字符串连接大概需要 0.125 s,而使用 join 缩短为 0.016s。
from time import time t = time() s = "" list = ['a','b','b','d','e','f','g','h','i','j','k','l','m','n'] for i in range (10000): for substr in list: s+= substr print "total run time:" print time()-t
使用join合并迭代器中的字符串
In [1]: %%timeit...: s = ''
...: for i in a:
...: s += i
...:
10000 loops, best of 3: 59.8 µs per loop
In [2]: %%timeit
s = ''.join(a)
...:
100000 loops, best of 3: 11.8 µs per loop
join对于累加的方式,有大约5倍的提升。
同时要避免:
s = "" for x in list: s += func(x)而是要使用:
slist = [func(elt) for elt in somelist] s = "".join(slist)
对字符串可以使用正则表达式或者内置函数来处理的时候,选择内置函数。
如 str.isalpha(),str.isdigit(),str.startswith(('x', 'yz')),str.endswith(('x', 'yz'))
选择合适的格式化字符方式
s1, s2 = 'ax', 'bx'%timeit -n 100000 'abc%s%s' % (s1, s2)
%timeit -n 100000 'abc{0}{1}'.format(s1, s2)
%timeit -n 100000 'abc' + s1 + s2
100000 loops, best of 3: 183 ns per loop
100000 loops, best of 3: 169 ns per loop
100000 loops, best of 3: 103 ns per loop
三种情况中,%的方式是最慢的,但是三者的差距并不大(都非常快)。(小数据测试好像没那么准)
对字符进行格式化比直接串联读取要快,因此要使用
out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)
而避免
out = "<html>" + head + prologue + query + tail + "</html>"
使用列表解析(list comprehension)和生成器表达式(generator expression)
列表解析要比在循环中重新构建一个新的 list 更为高效,因此我们可以利用这一特性来提高运行的效率。
from time import time t = time() list = ['a','b','is','python','jason','hello','hill','with','phone','test', 'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd'] total=[] for i in range (1000000): for w in list: total.append(w) print "total run time:" print time()-t
使用列表解析:
for i in range (1000000): a = [w for w in list]
上述代码直接运行大概需要 17s,而改为使用列表解析后 ,运行时间缩短为 9.29s。将近提高了一半。生成器表达式则是在 2.4 中引入的新内容,语法和列表解析类似,但是在大数据量处理时,生成器表达式的优势较为明显,它并不创建一个列表,只是返回一个生成器,因此效率较高。在上述例子上中代码 a = [w for w in list] 修改为 a = (w for w in list),运行时间进一步减少,缩短约为 2.98s。
避免不必要的数据结构或是数据拷贝
有时候程序员会有点儿走神,在不该用到数据结构的地方去用数据结构。例如,有人可能会写这样的的代码:
1
2
|
values
=
[x
for
x
in
sequence]
squares
=
[x
*
x
for
x
in
values]
|
也许他这么写是为了先得到一个列表,然后再在这个列表上进行一些操作。但是第一个列表是完全没有必要写在这里的。我们可以简单地把代码写成这样就行了:
1
|
squares
=
[x
*
x
for
x
in
sequence]
|
有鉴于此,你要小心那些偏执程序员所写的代码了,这些程序员对Python的值共享机制非常偏执。函数copy.deepcopy()的滥用也许是一个信号,表明该代码是由菜鸟或者是不相信Python内存模型的人所编写的。在这样的代码里,减少copy的使用也许会比较安全。
任何时候当你想给你的代码添加其他处理逻辑,比如添加装饰器,属性或是描述符,你都是在拖慢你的程序。例如,考虑这样一个类:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
A:
def
__init__(
self
, x, y):
self
.x
=
x
self
.y
=
y
@property
def
y(
self
):
return
self
._y
@y
.setter
def
y(
self
, value):
self
._y
=
value
|
现在,让我们简单地测试一下:
1
2
3
4
5
6
7
|
>>>
from
timeit
import
timeit
>>> a
=
A(
1
,
2
)
>>> timeit(
'a.x'
,
'from __main__ import a'
)
0.07817923510447145
>>> timeit(
'a.y'
,
'from __main__ import a'
)
0.35766440676525235
>>>
|
正如你所看到的,我们访问属性y比访问简单属性x不是慢了一点点,整整慢了4.5倍之多。如果你在乎性能的话,你就很有必要问一下你自己,对y的那些额外的定义是否都是必要的了。如果不是的话,那么你应该把那些额外的定义删掉,用一个简单的属性就够了。如果只是因为在其他语言里面经常使用getter和setter函数的话,你完全没有必要在Python中也使用相同的编码风格。
函数和模块优化
内置函数
build in 函数通常较快,add(a,b) 要优于 a+b。
使用函数
许多开发者刚开始的时候会将Python作为一个编写简单脚本的工具。当编写脚本的时候,很容易就会写一些没有结构的代码出来。
1
2
3
4
5
|
import
sys
import
csv
with
open
(sys.argv[
1
]) as f:
for
row
in
csv.reader(f):
# Some kind of processing
|
然而定义在全局范围内的代码要比定义在函数中的代码执行地慢。他们之间速度的差别是因为局部变量与全局变量不同的实现所引起的(局部变量的操作要比全局变量来得快)。所以,如果你想要让程序更快地运行,那么你可以简单地将代码放在一个函数中:
1
2
3
4
5
6
7
8
|
import
sys
import
csv
def
main(filename):
with
open
(filename) as f:
for
row
in
csv.reader(f):
# Some kind of processing
...
main(sys.argv[
1
])
|
这样操作以后,处理速度会有提升,但是这个提升的程度依赖于程序的复杂性。根据经验来讲,通常都会提升15%到30%之间。
选择性地减少属性的访问
当使用点(.)操作符去访问属性时都会带来一定的消耗。本质上来讲,这会触发一些特殊方法的执行,比如__getattribute__()和__getattr__(),这通常都会导致去内存中字典数据的查询。
可以通过两种方式来避免属性的访问,第一种是使用from module import name的方式。第二种是将对象的方法名保存下来,在调用时直接使用。
1
2
3
4
5
6
7
8
9
10
|
import
math
def
compute_roots(nums):
result
=
[]
for
n
in
nums:
result.append(math.sqrt(n))
return
result
# Test
nums
=
range
(
1000000
)
for
n
in
range
(
100
):
r
=
compute_roots(nums)
|
上面的代码在我的计算机上运行大概需要40秒的时间。把上面代码中的compute_roots()函数改写一下:
1
2
3
4
5
6
7
8
9
10
11
|
from
math
import
sqrt
def
compute_roots(nums):
result
=
[]
result_append
=
result.append
for
n
in
nums:
result_append(sqrt(n))
return
result
nums
=
range
(
1000000
)
for
n
in
range
(
100
):
r
=
compute_roots(nums)
|
这个版本的代码执行一下大概需要29秒。
这两个版本的代码唯一的不同之处在于后面一个版本减少了对属性的访问。然而,有必要强调一点是说,这种方式的优化仅仅针对经常运行的代码有效,比如循环。
使用局部变量
局部变量比全局变量,内建类型以及属性快。
理解变量的局部性
上面已经讲过,局部变量的操作比全局变量来得快。对于经常要访问的变量来说,最好把他们保存成局部变量。例如,考虑刚才已经讨论过的compute_roots()函数修改版:
1
2
3
4
5
6
7
8
9
|
import
math
def
compute_roots(nums):
sqrt
=
math.sqrt
result
=
[]
result_append
=
result.append
for
n
in
nums:
result_append(sqrt(n))
return
result
|
在这个版本中,sqrt函数被一个局部变量所替代。如果你执行这段代码的话,大概需要25秒就执行完了(前一个版本需要29秒)。
局部性原来同样适用于类的参数。通常来讲,使用self.name要比直接访问局部变量来得慢。在内部循环中,我们可以将经常要访问的属性保存为一个局部变量。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#Slower
class
SomeClass:
...
def
method(
self
):
for
x
in
s:
op(
self
.value)
# Faster
class
SomeClass:
...
def
method(
self
):
value
=
self
.value
for
x
in
s:
op(value)
|
降低方法调用次数
如果你有一个列表需要操作,传递整个列表,而不是遍历整个列表并且传递每个元素给函数并返回。
使用内联函数
在耗时较多的循环中,可以把函数的调用改为内联的方式;
合理使用copy与deepcopy
对于dict和list等数据结构的对象,直接赋值使用的是引用的方式。而有些情况下需要复制整个对象,这时可以使用copy包里的copy和deepcopy,这两个函数的不同之处在于后者是递归复制的。效率也不一样:(以下程序在ipython中运行)import copy
a = range(100000)
%timeit -n 10 copy.copy(a) # 运行10次 copy.copy(a)
%timeit -n 10 copy.deepcopy(a)
10 loops, best of 3: 1.55 ms per loop
10 loops, best of 3: 151 ms per loop
timeit后面的-n表示运行的次数,后两行对应的是两个timeit的输出,下同。由此可见后者慢一个数量级。
区别参考[python模块 - copy模块]
使用 cProfile, cStringIO 和 cPickle等用c实现相同功能
(分别对应profile, StringIO, pickle)的包
import cPickle
import pickle
a = range(10000)
%timeit -n 100 x = cPickle.dumps(a)
%timeit -n 100 x = pickle.dumps(a)
100 loops, best of 3: 1.58 ms per loop
100 loops, best of 3: 17 ms per loop
由c实现的包,速度快10倍以上!
使用最佳的反序列化方式
下面比较了eval, cPickle, json方式三种对相应字符串反序列化的效率:import json
import cPickle
a = range(10000)
s1 = str(a)
s2 = cPickle.dumps(a)
s3 = json.dumps(a)
%timeit -n 100 x = eval(s1)
%timeit -n 100 x = cPickle.loads(s2)
%timeit -n 100 x = json.loads(s3)
100 loops, best of 3: 16.8 ms per loop
100 loops, best of 3: 2.02 ms per loop
100 loops, best of 3: 798 µs per loop
可见json比cPickle快近3倍,比eval快20多倍。
表达式优化
使用**而不是pow
%timeit -n 10000 c = pow(2,20)%timeit -n 10000 c = 2**20
10000 loops, best of 3: 284 ns per loop
10000 loops, best of 3: 16.9 ns per loop
**就是快10倍以上!
交换两个变量的值使用
a,b=b,a 而不是借助中间变量 t=a;a=b;b=t;
>>> from timeit import Timer
>>> Timer("t=a;a=b;b=t","a=1;b=2").timeit()
0.25154118749729365
>>> Timer("a,b=b,a","a=1;b=2").timeit()
0.17156677734181258
使用if is
if done is not None 比语句 if done != None 更快;
[stackoverflow上关于Python里使用"if not =="和"!="的讨论]
[reddit上关于Python里使用"is"和"=="的讨论]
但是[
]使用级联比较
"x < y < z" 而不是 "x < y and y < z";
Python加快查找速度-作用域
提升python代码5%的执行速度。5%!同时也会触怒任何维护你代码的人。
但实际上,这篇文章只是解释一下你偶尔会在标准库或者其他人的代码中碰到的代码。我们先看一个标准库的例子,collections.OrderedDict
类:
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
return dict_setitem(self, key, value)
注意最后一个参数:dict_setitem=dict.__setitem__
。如果你仔细想就会感觉有道理。将值关联到键上,你只需要给__setitem__
传递三个参数:要设置的键,与键关联的值,传递给内建dict类的__setitem__
类方法。等会,好吧,也许最后一个参数没什么意义。
作用域查询
为了理解到底发生了什么,我们看下作用域。从一个简单问题开始:在一个python函数中,如果遇到了一个名为open
的东西,python如何找出open
的值?
# <GLOBAL: bunch of code here>
def myfunc():
# <LOCAL: bunch of code here>
with open('foo.txt', 'w') as f:
pass
简单作答:如果不知道GLOBAL和LOCAL的内容,你不可能确定
open
的值。概念上,python查找名称时会检查3个命名空间(简单起见忽略嵌套作用域):
- 局部命名空间
- 全局命名空间
- 内建命名空间
所以在myfunc
函数中,如果尝试查找open
的值时,我们首先会检查本地命名空间,然后是全局命名空间,接着内建命名空间。如果在这3个命名空间中都找不到open
的定义,就会引发NameError
异常。
作用域查找的实现
上面的查找过程只是概念上的。这个查找过程的实现给予了我们探索实现的空间。
1
2
3
4
5
6
7
8
9
|
def
foo():
a
=
1
return
a
def
bar():
return
a
def
baz(a
=
1
):
return
a
|
我们看下每个函数的字节码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
>>>
import
dis
>>> dis.dis(foo)
2
0
LOAD_CONST
1
(
1
)
3
STORE_FAST
0
(a)
3
6
LOAD_FAST
0
(a)
9
RETURN_VALUE
>>> dis.dis(bar)
2
0
LOAD_GLOBAL
0
(a)
3
RETURN_VALUE
>>> dis.dis(baz)
2
0
LOAD_FAST
0
(a)
3
RETURN_VALUE
|
注意foo和bar的区别。我们立即就可以看到,在字节码层面,python已经判断了什么是局部变量、什么不是,因为foo
使用LOAD_FAST
,而bar
使用LOAD_GLOBAL
。
我们不会具体阐述python的编译器如何知道何时生成何种字节码(也许那是另一篇文章的范畴了),但足以理解,python在执行函数时已经知道进行何种类型的查找。
另一个容易混淆的是,LOAD_GLOBAL
既可以用于全局,也可以用于内建命名空间的查找。忽略嵌套作用域的问题,你可以认为这是“非局部的”。对应的C代码大概是[1]:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
case LOAD_GLOBAL:
v
=
PyObject_GetItem(f
-
>f_globals, name);
if
(v
=
=
NULL) {
v
=
PyObject_GetItem(f
-
>f_builtins, name);
if
(v
=
=
NULL) {
if
(PyErr_ExceptionMatches(PyExc_KeyError))
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG, name);
goto error;
}
}
PUSH(v);
|
即使你从来没有看过CPython的C代码,上面的代码已经相当直白了。首先,检查我们查找的键名是否在f->f_globals
(全局字典)中,然后检查名称是否在f->f_builtins
(内建字典)中,最后,如果上面两个位置都没找到,就会抛出NameError
异常。
将常量绑定到局部作用域
现在我们再看最开始的代码例子,就会理解最后一个参数其实是将一个函数绑定到局部作用域中的一个函数上。具体是通过将dict.__setitem__
赋值为参数的默认值。这里还有另一个例子:
1
2
3
4
5
|
def
not_list_or_dict(value):
return
not
(
isinstance
(value,
dict
)
or
isinstance
(value,
list
))
def
not_list_or_dict(value, _isinstance
=
isinstance
, _dict
=
dict
, _list
=
list
):
return
not
(_isinstance(value, _dict)
or
_isinstance(value, _list))
|
这里我们做同样的事情,把本来将会在内建命名空间中的对象绑定到局部作用域中去。因此,python将会使用LOCAL_FAST
而不是LOAD_GLOBAL
(全局查找)。那么这到底有多快呢?我们做个简单的测试:
1
2
3
4
|
$ python
-
m timeit
-
s
'def not_list_or_dict(value): return not (isinstance(value, dict) or isinstance(value, list))'
'not_list_or_dict(50)'
1000000
loops, best of
3
:
0.48
usec per loop
$ python
-
m timeit
-
s
'def not_list_or_dict(value, _isinstance=isinstance, _dict=dict, _list=list): return not (_isinstance(value, _dict) or _isinstance(value, _list))'
'not_list_or_dict(50)'
1000000
loops, best of
3
:
0.423
usec per loop
|
换句话说,大概有11.9%的提升 [2]!
还有更多内涵
可以合理地认为,速度提升在于LOAD_FAST
读取局部作用域,而LOAD_GLOBAL
在检查内建作用域之前会先首先检查全局作用域。上面那个示例函数中,isinstance
、dict
、list
都位于内建命名空间。
但是,还有更多。我们不仅可以使用LOAD_FAST
跳过多余的查找,它也是一种不同类型的查找。
上面C代码片段给出了LOAD_GLOBAL
的代码,下面是LOAD_FAST
的:
1
2
3
4
5
6
7
8
9
10
11
|
case
LOAD_FAST:
PyObject *value = fastlocal[oparg];
if
(value == NULL) {
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
goto
error;
}
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH()
|
我们通过索引一个数组获取局部值。虽然没有直接出现,但是oparg
只是那个数组的一个索引。
现在听起来才合理。我们第一个版本的not_list_or_dict
要进行4个查询,每个名称都位于内建命名空间,它们只有在查找全局命名空间之后才会查询。这就是8个字典键的查询操作了。相比之下,not_list_or_dict
的第二版中,直接索引C数组4次,底层全部使用LOAD_FAST
。这就是为什么局部查询更快的原因。
总结
现在当下次你在其他人代码中看到这种例子,就会明白了。
最后,除非确实需要,请不要在具体应用中进行这类优化。而且大部分时间你都没必要做。但是如果时候到了,你需要挤出最后一点性能,就需要搞懂这点。
[1]注意,为了更易读,上面的代码中我去掉了一些性能优化。真正的代码稍微有点复杂。
[2]示例函数事实上没有做什么有价值的东西,也没进行IO操作,大部分是受python VM循环的限制。
[http://python.jobbole.com/81210/?from=singlemessage&isappinstalled=0]
其他优化技巧
- 使用 cProfile,cStringIO 和 cPickle: 一直使用 C 版本的模块
- 校验 a in b, 字典 或 set 比 列表 或 元组 更好
- 当数据量大的时候,尽可能使用不可变数据类型,他们更快 元组 > 列表
- 如果你需要操作列表的两端,使用 deque
- del – 删除对象使用如下
1) python 自己处理它,但确保使用了 gc 模块
2) 编写 __del__ 函数
3) 最简单的方式,使用后调用 del - GIL(http://wiki.python.org/moin/GlobalInterpreterLock) – GIL is a daemon GIL 仅仅允许一个 Python 的原生线程来运行每个进程。阻止 CPU 级别的并行,尝试使用 ctypes 和 原生的 C 库来解决它,当你达到 Python 优化的最后,总是存在一个选项,可以使用原生的 C 重写慢的函数,通过 Python 的 C 绑定使用它,其他的库如 gevent 也是致力于解决这个问题,并且获得了成功。 TL,DR:当你写代码了,过一遍数据结构,迭代结构,内建和为 GIL 创建 C 扩展,如有必要。 multiprocessing 是在 GIL 的范围之外,这意味着你可以使用 multiprocessing 这个标准库来运行多个进程。
最后,但是也很重要的是,请牢记John Ousterhout(译者注:Tcl和Tk的发明者,现为斯坦福大学计算机系的教授)说过的话“将不工作的东西变成能够工作的,这才是最大的性能提升”。在你需要优化前不要过分地考虑程序的优化工作。程序的正确性通常来讲都比程序的性能要来的重要。
[www.slideshare.net/atmb4u/faster-python]
Python 性能优化工具
Python 性能优化除了改进算法,选用合适的数据结构之外,还有几种关键的技术,比如将关键 python 代码部分重写成 C 扩展模块,或者选用在性能上更为优化的解释器等,这些在本文中统称为优化工具。python 有很多自带的优化工具,如 Psyco,Pypy,Cython,Pyrex 等。
如果你想让你的程序性能有质的飞跃的话,你可以去研究下基于JIT技术的工具。比如,PyPy项目,该项目是Python解释器的另一种实现,它能够分析程序的执行并为经常执行的代码生成机器码,有时它甚至能够让Python程序的速度提升一个数量级,达到(甚至超过)C语言编写的代码的速度。但是不幸的是,在本文正在写的时候,PyPy还没有完全支持Python 3。所以,我们还是在将来再来看它到底会发展的怎么样。基于JIT技术的还有Numba项目。该项目实现的是一个动态的编译器,你可以将你想要优化的Python函数以注解的方式进行标记,然后这些代码就会在LLVM的帮助下被编译成机器码。该项目也能够带来极大的性能上的提升。然而,就像PyPy一样,该项目对Python 3的支持还只是实验性的。
Note:使用C扩展(Extension)
目前主要有CPython(python最常见的实现的方式)原生API, ctypes,Cython,cffi三种方式,它们的作用是使得Python程序可以调用由C编译成的动态链接库,其特点分别是:
CPython原生API: 通过引入Python.h头文件,对应的C程序中可以直接使用Python的数据结构。实现过程相对繁琐,但是有比较大的适用范围。
ctypes: 通常用于封装(wrap)C程序,让纯Python程序调用动态链接库(Windows中的dll或Unix中的so文件)中的函数。如果想要在python中使用已经有C类库,使用ctypes是很好的选择,有一些基准测试下,python2+ctypes是性能最好的方式。
Cython: Cython是CPython的超集,用于简化编写C扩展的过程。Cython的优点是语法简洁,可以很好地兼容numpy等包含大量C扩展的库。Cython的使得场景一般是针对项目中某个算法或过程的优化。在某些测试中,可以有几百倍的性能提升。
cffi: cffi的就是ctypes在pypy(详见下文)中的实现,同进也兼容CPython。cffi提供了在python使用C类库的方式,可以直接在python代码中编写C代码,同时支持链接到已有的C类库。
使用这些优化方式一般是针对已有项目性能瓶颈模块的优化,可以在少量改动原有项目的情况下大幅度地提高整个程序的运行效率。
Psyco ["可爱的 Python: 用 Psyco 让 Python 运行得像 C 一样快"使用 Psyco 提高效率]
psyco 是一个 just-in-time 的编译器,它能够在不改变源代码的情况下提高一定的性能,Psyco 将操作编译成有点优化的机器码,其操作分成三个不同的级别,有"运行时"、"编译时"和"虚拟时"变量。并根据需要提高和降低变量的级别。运行时变量只是常规 Python 解释器处理的原始字节码和对象结构。一旦 Psyco 将操作编译成机器码,那么编译时变量就会在机器寄存器和可直接访问的内存位置中表示。同时 python 能高速缓存已编译的机器码以备今后重用,这样能节省一点时间。但 Psyco 也有其缺点,其本身运行所占内存较大。目前 psyco 已经不在 python2.7 中支持,而且不再提供维护和更新了,对其感兴趣的可以参考 http://psyco.sourceforge.net/
Pypy [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 需要先下载https://bitbucket.org/pypy/pypy/downloads/pypy-1.8-win32.zip,然后解压到相关的目录,并将解压后的路径添加到环境变量 path 中即可。在命令行运行 pypy,如果出现如下错误:"没有找到 MSVCR100.dll, 因此这个应用程序未能启动,重新安装应用程序可能会修复此问题",则还需要在微软的官网上下载 VS 2010 runtime libraries 解决该问题。具体地址为http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=5555
Note:PyPy是用RPython(CPython的子集)实现的Python,根据官网的基准测试数据,它比CPython实现的Python要快6倍以上。快的原因是使用了Just-in-Time(JIT)编译器,即动态编译器,与静态编译器(如gcc,javac等)不同,它是利用程序运行的过程的数据进行优化。由于历史原因,目前pypy中还保留着GIL,不过正在进行的STM项目试图将PyPy变成没有GIL的Python。如果python程序中含有C扩展(非cffi的方式),JIT的优化效果会大打折扣,甚至比CPython慢(比Numpy)。所以在PyPy中最好用纯Python或使用cffi扩展。随着STM,Numpy等项目的完善,相信PyPy将会替代CPython。
安装成功后在命令行里运行 pypy,输出结果如下:
C:\Documents and Settings\Administrator>pypy Python 2.7.2 (0e28b379d8b3, Feb 09 2012, 18:31:47) [PyPy 1.8.0 with MSC v.1500 32 bit] on win32 Type "help", "copyright", "credits" or "license" for more information. And now for something completely different: ``PyPy is vast, and contains multitudes'' >>>>
以清单 5 的循环为例子,使用 python 和 pypy 分别运行,得到的运行结果分别如下:
C:\Documents and Settings\Administrator\ 桌面 \doc\python>pypy loop.py total run time: 8.42199993134 C:\Documents and Settings\Administrator\ 桌面 \doc\python>python loop.py total run time: 106.391000032
可见使用 pypy 来编译和运行程序,其效率大大的提高。
[既然PyPy的速度是CPython的6.3倍,难道我不应该放弃CPython转用PyPy?]
Cython [Cython 官网]
Cython 是用 python 实现的一种语言,可以用来写 python 扩展,用它写出来的库都可以通过 import 来载入,性能上比 python 的快。cython 里可以载入 python 扩展 ( 比如 import math),也可以载入 c 的库的头文件 ( 比如 :cdef extern from "math.h"),另外也可以用它来写 python 代码。将关键部分重写成 C 扩展模块
Linux Cpython 的安装:
第一步:下载
[root@v5254085f259 cpython]# wget -N http://cython.org/release/Cython-0.15.1.zip
第二步:解压
[root@v5254085f259 cpython]# unzip -o Cython-0.15.1.zip
第三步:安装
python setup.py install
安装完成后直接输入 cython,如果出现如下内容则表明安装成功。
[root@v5254085f259 Cython-0.15.1]# cython Cython (http://cython.org) is a compiler for code written in the Cython language. Cython is based on Pyrex by Greg Ewing. Usage: cython [options] sourcefile.{pyx,py} ... Options: -V, --version Display version number of cython compiler -l, --create-listing Write error messages to a listing file -I, --include-dir <directory> Search for include files in named directory (multiple include directories are allowed). -o, --output-file <filename> Specify name of generated C file -t, --timestamps Only compile newer source files -f, --force Compile all source files (overrides implied -t) -q, --quiet Don't print module names in recursive mode -v, --verbose Be verbose, print file names on multiple compil ation -p, --embed-positions If specified, the positions in Cython files of each function definition is embedded in its docstring. --cleanup <level> Release interned objects on python exit, for memory debugging. Level indicates aggressiveness, default 0 releases nothing. -w, --working <directory> Sets the working directory for Cython (the directory modules are searched from) --gdb Output debug information for cygdb -D, --no-docstrings Strip docstrings from the compiled module. -a, --annotate Produce a colorized HTML version of the source. --line-directives Produce #line directives pointing to the .pyx source --cplus Output a C++ rather than C file. --embed[=<method_name>] Generate a main() function that embeds the Python interpreter. -2 Compile based on Python-2 syntax and code seman tics. -3 Compile based on Python-3 syntax and code seman tics. --fast-fail Abort the compilation on the first error --warning-error, -Werror Make all warnings into errors --warning-extra, -Wextra Enable extra warnings -X, --directive <name>=<value> [,<name=value,...] Overrides a compiler directive
其他平台上的安装:可以参考文档:http://docs.cython.org/src/quickstart/install.html
Cython 代码与 python 不同,必须先编译,编译一般需要经过两个阶段,将 pyx 文件编译为 .c 文件,再将 .c 文件编译为 .so 文件。编译有多种方法:
- 通过命令行编译:
假设有如下测试代码,使用命令行编译为 .c 文件。
def sum(int a,int b): print a+b [root@v5254085f259 test]# cython sum.pyx [root@v5254085f259 test]# ls ... 60 -rw-r--r-- 1 root root 55169 Apr 17 02:45 sum.c 4 -rw-r--r-- 1 root root 35 Apr 17 02:45 sum.pyx
在 linux 上利用 gcc 编译为 .so 文件:
[root@v5254085f259 test]# gcc -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing -I/usr/include/python2.4 -o sum.so sum.c [root@v5254085f259 test]# ls ... 60 -rw-r--r-- 1 root root 55169 Apr 17 02:45 sum.c 4 -rw-r--r-- 1 root root 35 Apr 17 02:45 sum.pyx 20 -rwxr-xr-x 1 root root 20307 Apr 17 02:47 sum.so
- 使用 distutils 编译
建立一个 setup.py 的脚本:
from distutils.core import setup from distutils.extension import Extension from Cython.Distutils import build_ext ext_modules = [Extension("sum", ["sum.pyx"])] setup( name = 'sum app', cmdclass = {'build_ext': build_ext}, ext_modules = ext_modules ) [root@v5254085f259 test]# python setup.py build_ext --inplace running build_ext cythoning sum.pyx to sum.c building 'sum' extension gcc -pthread -fno-strict-aliasing -fPIC -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/opt/ActivePython-2.7/include/python2.7 -c sum.c -o build/temp.linux-x86_64-2.7/sum.o gcc -pthread -shared build/temp.linux-x86_64-2.7/sum.o -o /root/cpython/test/sum.so
编译完成之后可以导入到 python 中使用:
[root@v5254085f259 test]# python >>> import pyximport; pyximport.install() >>> import sum >>> sum.sum(1,3)
简单的性能比较:
Cython 测试代码
from time import time def test(int n): cdef int a =0 cdef int i for i in xrange(n): a+= i return a t = time() test(10000000) print "total run time:" 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 测试代码
from time import time def test(n): a =0; for i in xrange(n): a+= i return a t = time() test(10000000) print "total run time:" print time()-t [root@v5254085f259 test]# python test.py total run time: 0.971596002579
从上述对比可以看到使用 Cython 的速度提高了将近 100 多倍。
[6 Python Performance Tips]
用Cython加速不可向量化代码
在Python中使用数组时,向量化操作会比循环操作要快得多。但是当没有明显能向量化一个很慢的函数的方法时怎么办呢?
[各种 Python 实现的简单介绍与比较:CPython、Jython、IronPython、PyPy、Pyston]
numpy性能优化
Optimizing Python - a Case Study
ref:Python性能优化的20条建议*
Python里高效非标准数据结构集锦,包括Bloom Filters、Arrays、Linked Lists、Tries、Graphs & Specialized Automata等