Python性能优化

发表于:2017-8-24 10:22

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Python开发者    来源:51Testing软件测试网采编

  profile针对优化
  知道了热点,就可以进行针对性的优化,而这个优化往往根具体的业务密切相关,没用万能钥匙,具体问题,具体分析。个人经验而言,最有效的优化是找产品经理讨论需求,可能换一种方式也能满足需求,少者稍微折衷一下产品经理也能接受。次之是修改代码的实现,比如之前使用了一个比较通俗易懂但效率较低的算法,如果这个算法成为了性能瓶颈,那就考虑换一种效率更高但是可能难理解的算法、或者使用dirty Flag模式。对于这些同样的方法,需要结合具体的案例,本文不做赘述。
  接下来结合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有详细的描述。下面看一个简单的测试例子:
  class BaseSlots(object): 
  __slots__ = ['e', 'f', 'g'] 
  class Slots(BaseSlots): 
  __slots__ = ['a', 'b', 'c', 'd'] 
  def __init__(self): 
  self.a = self.b = self.c = self.d = self.e = self.f = self.g = 0 
  class BaseNoSlots(object): 
  pass 
  class NoSlots(BaseNoSlots): 
  def __init__(self): 
  super(NoSlots,self).__init__() 
  self.a = self.b = self.c = self.d = self.e = self.f = self.g = 0 
  def log_time(s): 
  begin = time.time() 
  for i in xrange(10000000): 
  s.a,s.b,s.c,s.d, s.e, s.f, s.g 
  return time.time() - begin 
  if __name__ == '__main__': 
  print 'Slots cost', log_time(Slots()) 
  print '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
  前面提到cython可以用到binding c扩展,但是其作用远远不止这一点。
  Cython的主要目的是加速python的运行效率,但是又不像上一章节提到的C扩展那么复杂。在Cython中,写C扩展和写python代码的复杂度差不多(多亏了Pyrex)。Cython是python语言的超集,增加了对C语言函数调用和类型声明的支持。从这个角度来看,cython将动态的python代码转换成静态编译的C代码,这也是cython高效的原因。使用cython同C扩展一样,需要编译成动态链接库,在linux环境下既可以用命令行,也可以用distutils。
  如果想要系统学习cython,建议从cython document入手,文档写得很好。下面通过一个简单的示例来展示cython的使用方法和性能(linux环境)。
  首先,安装cython:
  pip install Cython
  下面是测试用的python代码,可以看到这两个case都是运算复杂度比较高的例子:
  def f(): 
  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
  不改动任何python代码也可以享受到cython带来的性能提升,具体做法如下:
  step1:将文件名(cython_example.py)改为cython_example.pyx
  step2:增加一个setup.py文件,添加一下代码:
  from distutils.core import setup 
  from Cython.Build import cythonize 
  setup( 
  name = 'cython_example', 
  ext_modules = cythonize("cython_example.pyx"), 
  )
  step3:执行python setup.py build_ext –inplace
  可以看到 增加了两个文件,对应中间结果和最后的动态链接库
  step4:执行命令 python -c “import cython_example;cython_example.main()”(注意: 保证当前环境下已经没有 cython_example.py)
  运行结果:
  call f cost: 0.0874309539795 
  call integrate_f cost: 2.92381191254
  性能提升了大概两倍,我们再来试试cython提供的静态类型(static typing),修改cython_example.pyx的核心代码,替换f()和integrate_f()的实现如下:
  def f(double x): # 参数静态类型 
  return x**2-x 
  def integrate_f(double a, double b, int N): 
  cdef int i 
  cdef double s, dx 
  s = 0 
  dx = (b-a)/N 
  for i in range(N): 
  s += f(a+i*dx) 
  return s * dx
  然后重新运行上面的第三 四步:结果如下
  call f cost: 0.042387008667 
  call integrate_f cost: 0.958620071411
  上面的代码,只是对参数引入了静态类型判断,下面对返回值也引入静态类型判断。
  替换f()和integrate_f()的实现如下:
  cdef double f(double x): # 返回值也有类型判断 
  return x**2-x 
  cdef double integrate_f(double a, double b, int N): 
  cdef int i 
  cdef double s, dx 
  s = 0 
  dx = (b-a)/N 
  for i in range(N): 
  s += f(a+i*dx) 
  return s * dx
  然后重新运行上面的第三 四步:结果如下
  call f cost: 1.19209289551e-06 
  call integrate_f cost: 0.187038183212
  Amazing!
  pypy
  pypy是CPython的一个替代实现,其最主要的优势就是pypy的速度,下面是官网的测试结果:
  在实际项目中测试,pypy大概比cpython要快3到5倍!pypy的性能提升来自JIT Compiler。在前文提到google的Unladen Swallow 项目也是想在CPython中引入JIT,在这个项目失败后,很多开发人员都开始加入pypy的开发和优化。另外pypy占用的内存更少,而且支持stackless,基本等同于协程。
  pypy的缺点在于对C扩展方面支持的不太好,需要使用CFFi来做binding。对于使用广泛的library来说,一般都会支持pypy,但是小众的、或者自行开发的C扩展就需要重新封装了。
  ChangeLog
  2017.03.10 增加了对__slots__的介绍
  references
  编程语言benchmark
  python属性查找
  python profiler
  yappi
  greenletprofiler
  python-profiling-tools
  python C API
  cython
  Pyrex
  cython document
  pypy
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号