python性能优化
注意:本⽂除⾮特殊指明,”python“都是代表CPython,即C语⾔实现的标准python,且本⽂所讨论的是版本为2.7的CPython。另外,本⽂会不定期更新,如果⼤家有⼀些好的想法,请在评论⾥⾯留⾔,我会补充到⽂章中去。
姊妹篇:《》
姊妹篇:《》
python为什么性能差:
  当我们提到⼀门编程语⾔的效率时:通常有两层意思,第⼀是开发效率,这是对程序员⽽⾔,完成编码所需要的时间;另⼀个是运⾏效率,这是对计算机⽽⾔,完成计算任务所需要的时间。编码效率和运⾏效率往往是鱼与熊掌的关系,是很难同时兼顾的。不同的语⾔会有不同的侧重,python语⾔毫⽆疑问更在乎编码效率,life is short,we use python。
  虽然使⽤python的编程⼈员都应该接受其运⾏效率低的事实,但python在越多越来的领域都有⼴泛应⽤,⽐如科学计算、web服务器等。程序员当然也希望python能够运算得更快,希望python可以更强⼤。
  ⾸先,python相⽐其他语⾔具体有多慢,这个不同场景和测试⽤例,结果肯定是不⼀样的。给出了不同语⾔在各种case下的性能对⽐,是python3和C++的对⽐,下⾯是两个case:
  从上图可以看出,不同的case,python⽐C++慢了⼏倍到⼏⼗倍。
  python运算效率低,具体是什么原因呢,下列罗列⼀些
  第⼀:python是动态语⾔
  ⼀个变量所指向对象的类型在运⾏时才确定,编译器做不了任何预测,也就⽆从优化。举⼀个简单的例⼦: r = a + b。 a和b相加,但a和b的类型在运⾏时才知道,对于加法操作,不同的类型有不同的处理,所以每次运⾏的时候都会去判断a和b的类型,然后执⾏对应的操作。⽽在静态语⾔如C++中,编译的时候就确定了运⾏时的代码。
  另外⼀个例⼦是属性查,关于具体的查顺序在中有详细介绍。简⽽⾔之,访问对象的某个属性是⼀个⾮常复杂的过程,⽽且通过同⼀个变量访问到的python对象还都可能不⼀样(参见Lazy property的例⼦)。⽽在C语⾔中,访问属性⽤对象的地址加上属性的偏移就可以了。
  第⼆:python是解释执⾏,但是不⽀持JIT(just in time compiler)。虽然⼤名⿍⿍的google曾经尝试这个项⽬,但最终也折了。
  第三:python中⼀切都是对象,每个对象都需要维护引⽤计数,增加了额外的⼯作。
  第四:python GIL
  GIL是Python最为诟病的⼀点,因为GIL,python中的多线程并不能真正的并发。如果是在IO bound的业务场景,这个问题并不⼤,但是在CPU BOUND的场景,这就很致命了。所以笔者在⼯作中使⽤pytho
n多线程的情况并不多,⼀般都是使⽤多进程(pre fork),或者在加上协程。即使在单线程,GIL也会带来很⼤的性能影响,因为python每执⾏100个opcode(默认,可以通过sys.setcheckinterval()设置)就会尝试线程的切换,具体的源代码
在ceval.c::PyEval_EvalFrameEx。
  第五:垃圾回收,这个可能是所有具有垃圾回收的编程语⾔的通病。python采⽤标记和分代的垃圾回收策略,每次垃圾回收的时候都会中断正在执⾏的程序(stop the world),造成所谓的顿卡。上有⼀篇⽂章,提到禁⽤Python的GC机制后,Instagram性能提升了10%。感兴趣的读者可以去细读。
Be pythonic
  我们都知道过早的优化是罪恶之源,⼀切优化都需要基于profile。但是,作为⼀个python开发者应该要pythonic,⽽且pythonic的代码往往⽐non-pythonic的代码效率⾼⼀些,⽐如:
使⽤迭代器iterator,for example:
  dict的iteritems ⽽不是items(同itervalues,iterkeys)
  使⽤generator,特别是在循环中可能提前break的情况
判断是否是同⼀个对象使⽤ is ⽽不是 ==
判断⼀个对象是否在⼀个集合中,使⽤set⽽不是list
利⽤短路求值特性,把“短路”概率过的逻辑表达式写在前⾯。其他的也是可以的
对于⼤量字符串的累加,使⽤join操作
使⽤for else(while else)语法
交换两个变量的值使⽤: a, b = b, a
基于profile的优化
  即使我们的代码已经⾮常pythonic了,但可能运⾏效率还是不能满⾜预期。我们也知道80/20定律,绝⼤多数的时间都耗费在少量的代码⽚段⾥⾯了,优化的关键在于出这些瓶颈代码。⽅式很多:到处加log打印时间戳、或者将怀疑的函数使⽤timeit进⾏单独测试,但最有效的是使⽤profile⼯具。
python profilers
  对于python程序,⽐较出名的profile⼯具有三个:profile、cprofile和hotshot。其中profile是纯python语
⾔实现的,Cprofile将profile的部分实现native
化,hotshot hotshot已经停⽌维护了。需
要注意的是
时间
  对于协程(greenlet),可以使⽤,基于yappi修改,⽤greenlet context hook住thread context
  下⾯给出⼀段编造的”效率低下“的代码,并使⽤Cprofile来说明profile的具体⽅法以及我们可能遇到的性能瓶颈。
1# -*- coding: UTF-8 -*-
2
3from cProfile import Profile
4import math
5def foo():
python是做什么的通俗易懂的
6return foo1()
7
8def foo1():
9return foo2()
10
11def foo2():
12return foo3()
13
14def foo3():
15return foo4()
16
17def foo4():
18return"this call tree seems ugly, but it always happen"
19
20def bar():
21    ret = 0
22for i in xrange(10000):
23        ret += i * i + math.sqrt(i)
24return ret
25
26def main():
27for i in range(100000):
28if i % 10000 == 0:
29            bar()
30else:
31            foo()
32
33if__name__ == '__main__':
34    prof = Profile()
35    prof.runcall(main)
36    prof.print_stats()
37#prof.dump_stats('test.prof') # dump profile result to test.prof
code for profile
  运⾏结果如下:
  对于上⾯的的输出,每⼀个字段意义如下:
  ncalls 函数总的调⽤次数
  tottime 函数内部(不包括⼦函数)的占⽤时间
  percall(第⼀个) tottime/ncalls
  cumtime 函数包括⼦函数所占⽤的时间
  percall(第⼆个)cumtime/ncalls
  filename:lineno(function)  ⽂件:⾏号(函数)
  代码中的输出⾮常简单,事实上可以利⽤pstat,让profile结果的输出多样化,具体可以参见官⽅⽂档。
profile GUI tools
  虽然Cprofile的输出已经⽐较直观,但我们还是倾向于保存profile的结果,然后⽤图形化的⼯具来从不同的维度来分析,或者⽐较优化前后的代码。查看profile结果的⼯具也⽐较多,⽐如,、、,本⽂⽤visualpytune做分析。对于上⾯的代码,按照注释⽣成修改后重新运⾏⽣成test.prof⽂件,⽤visualpytune直接打开就可以了,如下:
  字段的意义与⽂本输出基本⼀致,不过便捷性可以点击字段名排序。左下⽅列出了当前函数的calller(调⽤者),右下⽅是当前函数内部与⼦函数的时间占⽤情况。上如是按照cumtime(即该函数内部及其⼦函数所占的时间和)排序的结果。
  造成性能瓶颈的原因通常是⾼频调⽤的函数、单次消耗⾮常⾼的函数、或者⼆者的结合。在我们前⾯的例⼦中,foo就属于⾼频调⽤的情况,bar属于单次消耗⾮常⾼的情况,这都是我们需要优化的重点。
  中介绍了qcachegrind和runsnakerun的使⽤⽅法,这两个colorful的⼯具⽐visualpytune强⼤得多。具体的使⽤⽅法请参考原⽂,下图给出test.prof⽤qcachegrind打开的结果
  qcachegrind确实要⽐visualpytune强⼤。从上图可以看到,⼤致分为三部:。第⼀部分同visualpytune类似,是每个函数占⽤的时间,其中Incl等同于
cumtime, Self等同于tottime。第⼆部分和第三部分都有很多标签,不同的标签标⽰从不同的⾓度来看结果,如图上所以,第三部分的“call graph”展⽰了该函数的call tree并包含每个⼦函数的时间百分⽐,⼀⽬了然。
profile针对优化
  知道了热点,就可以进⾏针对性的优化,⽽这个优化往往根具体的业务密切相关,没⽤,具体问题,具体分析。个⼈经验⽽⾔,最有效的优化是产品经理讨论需求,可能换⼀种⽅式也能满⾜需求,少者稍微折衷⼀下产品经理也能接受。次之是修改代码的实现,⽐如之前使⽤了⼀个⽐较通俗易懂但效率较低的算法,如果这个算法成为了性能瓶颈,那就考虑换⼀种效率更⾼但是可能难理解的算法、或者使⽤模式。对于这些同样的⽅法,需要结合具体的案例,本⽂不做赘述。
  接下来结合python语⾔特性,介绍⼀些让python代码不那么pythonic,但可以提升性能的⼀些做法
第⼀:减少函数的调⽤层次
每⼀层函数调⽤都会带来不⼩的开销,特别对于调⽤频率⾼,但单次消耗较⼩的calltree,多层的函数调
⽤开销就很⼤,这个时候可以考虑将其展开。
 对于之前调到的profile的代码,foo这个call tree⾮常简单,但频率⾼。修改代码,增加⼀个plain_foo()函数, 直接返回最终结果,关键输出如下:
  跟之前的结果对⽐:
  可以看到,优化了差不多3倍。
第⼆:优化属性查
上⾯提到,python 的属性查效率很低,如果在⼀段代码中频繁访问⼀个属性(⽐如for循环),那么可以考虑⽤局部变量代替对象的属性。
第三:关闭GC
  在本⽂的第⼀章节已经提到,关闭GC可以提升python的性能,GC带来的顿卡在实时性要求⽐较⾼的应⽤场景也是难以接受的。但关闭GC并不是⼀件容易的事情。我们知道python的引⽤计数只能应付没有循环引⽤的情况,有了循环引⽤就需要靠GC来处理。在python语⾔中, 写出循环引⽤⾮常容易。⽐如:
  case 1:
  a, b = SomeClass(), SomeClass()
a.b,
b.a = b, a
  case 2:
  lst = []
  lst.append(lst)
  case 3:
  self.handler = self.some_func
  当然,⼤家可能说,谁会这么傻,写出这样的代码,是的,上⾯的代码太明显,当中间多⼏个层级之后,就会出现“间接”的循环应⽤。在python的标准库collections⾥⾯的OrderedDict就是case2:
  要解决循环引⽤,第⼀个办法是使⽤弱引⽤(weakref),第⼆个是⼿动解循环引⽤。
第四:setcheckinterval
  如果程序确定是单线程,那么修改checkinterval为⼀个更⼤的值,有介绍。
第五:使⽤__slots__
  slots最主要的⽬的是⽤来节省内存,但是也能⼀定程度上提⾼性能。我们知道定义了__slots__的类,对某⼀个实例都会预留⾜够的空间,也就不会再⾃动创建__dict__。当然,使⽤__slots__也有许多注意事项,最重要的⼀点,继承链上的所有类都必须定义__slots__,python doc有详细的描述。下⾯看⼀个简单的测试例⼦:
1class BaseSlots(object):
2__slots__ = ['e', 'f', 'g']
3
4class Slots(BaseSlots):
5__slots__ = ['a', 'b', 'c', 'd']
6def__init__(self):
7        self.a = self.b = self.c = self.d = self.e = self.f  = self.g = 0
8
9class BaseNoSlots(object):
10pass
11
12class NoSlots(BaseNoSlots):
13def__init__(self):
14        super(NoSlots,self).__init__()
15        self.a = self.b = self.c = self.d = self.e = self.f  = self.g = 0
16
17def log_time(s):
18    begin = time.time()
19for i in xrange(10000000):
20        s.a,s.b,s.c,s.d, s.e, s.f, s.g
21return time.time() - begin
22
23if__name__ == '__main__':
24print'Slots cost', log_time(Slots())
25print'NoSlots cost', log_time(NoSlots())
  输出结果:
Slots cost 3.12999987602
NoSlots cost 3.48100018501
python C扩展
  也许通过profile,我们已经到了性能热点,但这个热点就是要运⾏⼤量的计算,⽽且没法cache,没法省略。。。这个时候就该python的C扩展出马
了,C扩展就是把部分python代码⽤C或者C++重新实现,然后编译成动态链接库,提供接⼝给其它python代码调⽤。由于C语⾔的效率远远⾼于python代码,所以使⽤C扩展是⾮常普遍的做法,⽐如我们前⾯提到的cProfile就是基于_lsprof.so的⼀层封装。python的⼤所属对性能有要求的库都使⽤或者提供了C扩展,如gevent、protobuff、bson。
  笔者曾经测试过纯python版本的bson和cbson的效率,在综合的情况下,cbson快了差不多10倍!
  python的C扩展也是⼀个⾮常复杂的问题,本⽂仅给出⼀些注意事项:
第⼀:注意引⽤计数的正确管理
  这是最难最复杂的⼀点。我们都知道python基于指针技术来管理对象的⽣命周期,如果在扩展中引⽤计数出了问题,那么要么是程序崩溃,要么是内存泄漏。更要命的是,引⽤计数导致的问题很难debug。。。
  C扩展中关于引⽤计数最关键的三个词是:steal reference,borrowed reference,new reference。建议编写扩展代码之前细读python的。
第⼆:C扩展与多线程
  这⾥的多线程是指在扩展中new出来的C语⾔线程,⽽不是python的多线程,出了python doc⾥⾯的介绍,也可以看看《python cookbook》的相关章节。
第三:C扩展应⽤场景
  仅适合与业务代码的关系不那么紧密的逻辑,如果⼀段代码⼤量业务相关的对象属性的话,是很难C扩展的
  将C扩展封装成python代码可调⽤的接⼝的过程称之为binding,Cpython本⾝就提供了⼀套原⽣的API,虽然使⽤最为⼴泛,但该规范⽐较复杂。很多第三⽅库做了不同程度的封装,以便开发者使⽤,⽐如boost.python、cython、ctypes、cffi(同时⽀持pypy cpython),具体怎么使⽤可以google。
beyond CPython
  尽管python的性能差强⼈意,但是其易学易⽤的特性还是赢得越来越多的使⽤者,业界⼤⽜也从来没有放弃对python的优化。这⾥的优化是对python语⾔设计上、或者实现上的⼀些反思或者增强。这些优化项⽬⼀些已经夭折,⼀些还在进⼀步改善中,在这个章节介绍⽬前还不错的⼀些项⽬。
cython
  前⾯提到可以⽤到binding c扩展,但是其作⽤远远不⽌这⼀点。
  Cython的主要⽬的是加速python的运⾏效率,但是⼜不像上⼀章节提到的C扩展那么复杂。在Cython中,写C扩展和写python代码的复杂度差不多(多亏了)。Cython是python语⾔的超集,增加了对C语⾔函数调⽤和类型声明的⽀持。从这个⾓度来看,cython将动态的python代码转换成静态编译的C代码,这也是cython⾼效的原因。使⽤cython同C扩展⼀样,需要编译成动态链接库,在linux环境下既可以⽤命令⾏,也可以⽤distutils。
  如果想要系统学习cython,建议从⼊⼿,⽂档写得很好。下⾯通过⼀个简单的⽰例来展⽰cython的使⽤⽅法和性能(linux环境)。
  ⾸先,安装cython:
  pip install Cython
  下⾯是测试⽤的python代码,可以看到这两个case都是运算复杂度⽐较⾼的例⼦:
# -*- coding: UTF-8 -*-
def f(x):
return x**2-x
def integrate_f(a, b, N):
s = 0
dx = (b-a)/N
for i in range(N):
s += f(a+i*dx)
return s * dx
def main():
import time
begin = time.time()
for i in xrange(10000):
for i in xrange(100):f(10)
print'call f cost:', time.time() - begin
begin = time.time()
for i in xrange(10000):
integrate_f(1.0, 100.0, 1000)
print'call integrate_f cost:', time.time() - begin
if__name__ == '__main__':
main()
  运⾏结果:
  call f cost: 0.215116024017
  call integrate_f cost: 4.33698010445

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。