Python性能优化

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

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

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

  正文
  注意:本文除非特殊指明,”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:
  python运算效率低,具体是什么原因呢,下列罗列一些
  第一:python是动态语言
  一个变量所指向对象的类型在运行时才确定,编译器做不了任何预测,也就无从优化。举一个简单的例子: r = a + b。 a和b相加,但a和b的类型在运行时才知道,对于加法操作,不同的类型有不同的处理,所以每次运行的时候都会去判断a和b的类型,然后执行对应的操作。而在静态语言如C++中,编译的时候就确定了运行时的代码。
  另外一个例子是属性查找,关于具体的查找顺序在《python属性查找》中有详细介绍。简而言之,访问对象的某个属性是一个非常复杂的过程,而且通过同一个变量访问到的python对象还都可能不一样(参见Lazy property的例子)。而在C语言中,访问属性用对象的地址加上属性的偏移就可以了。
  第二:python是解释执行,但是不支持JIT(just in time compiler)。虽然大名鼎鼎的google曾经尝试Unladen Swallow 这个项目,但最终也折了。
  第三:python中一切都是对象,每个对象都需要维护引用计数,增加了额外的工作
  第四:python GIL
  GIL是Python最为诟病的一点,因为GIL,python中的多线程并不能真正的并发。如果是在IO bound的业务场景,这个问题并不大,但是在CPU BOUND的场景,这就很致命了。所以笔者在工作中使用python多线程的情况并不多,一般都是使用多进程(pre fork),或者在加上协程。即使在单线程,GIL也会带来很大的性能影响,因为python每执行100个opcode(默认,可以通过sys.setcheckinterval()设置)就会尝试线程的切换,具体的源代码在ceval.c::PyEval_EvalFrameEx。
  第五:垃圾回收,这个可能是所有具有垃圾回收的编程语言的通病。
  python采用标记和分代的垃圾回收策略,每次垃圾回收的时候都会中断正在执行的程序,造成所谓的顿卡。infoq上有一篇文章,提到禁用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
  利用短路求值特性,把“短路”概率过的逻辑表达式写在前面。其他的lazy ideas也是可以的
  对于大量字符串的累加,使用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也是C语言实现,hotshot与Cprofile的区别在于:hotshot对目标代码的运行影响较小,代价是更多的后处理时间,而且hotshot已经停止维护了。需要注意的是,profile(Cprofile hotshot)只适合单线程的python程序。
  对于多线程,可以使用yappi,yappi不仅支持多线程,还可以精确到CPU时间
  对于协程(greenlet),可以使用greenletprofiler,基于yappi修改,用greenlet context hook住thread context
  下面给出一段编造的”效率低下“的代码,并使用Cprofile来说明profile的具体方法以及我们可能遇到的性能瓶颈。
  # -*- coding: UTF-8 -*- 
  from cProfile import Profile 
  import math 
  def foo(): 
  return foo1() 
  def foo1(): 
  return foo2() 
  def foo2(): 
  return foo3() 
  def foo3(): 
  return foo4() 
  def foo4(): 
  return "this call tree seems ugly, but it always happen" 
  def bar(): 
  ret = 0 
  for i in xrange(10000): 
  ret += i * i + math.sqrt(i) 
  return ret 
  def main(): 
  for i in range(100000): 
  if i % 10000 == 0: 
  bar() 
  else: 
  foo() 
  if __name__ == '__main__': 
  prof = Profile() 
  prof.runcall(main) 
  prof.print_stats() 
  #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结果的输出多样化,具体可以参见官方文档python profiler。
  profile GUI tools
  虽然Cprofile的输出已经比较直观,但我们还是倾向于保存profile的结果,然后用图形化的工具来从不同的维度来分析,或者比较优化前后的代码。查看profile结果的工具也比较多,比如,visualpytune、qcachegrind、runsnakerun,本文用visualpytune做分析。对于上面的代码,按照注释生成修改后重新运行生成test.prof文件,用visualpytune直接打开就可以了,如下:
  字段的意义与文本输出基本一致,不过便捷性可以点击字段名排序。左下方列出了当前函数的calller(调用者),右下方是当前函数内部与子函数的时间占用情况。上如是按照cumtime(即该函数内部及其子函数所占的时间和)排序的结果。
  造成性能瓶颈的原因通常是高频调用的函数、单次消耗非常高的函数、或者二者的结合。在我们前面的例子中,foo就属于高频调用的情况,bar属于单次消耗非常高的情况,这都是我们需要优化的重点。
  python-profiling-tools中介绍了qcachegrind和runsnakerun的使用方法,这两个colorful的工具比visualpytune强大得多。具体的使用方法请参考原文,下图给出test.prof用qcachegrind打开的结果。
  qcachegrind确实要比visualpytune强大。从上图可以看到,大致分为三部:。第一部分同visualpytune类似,是每个函数占用的时间,其中Incl等同于cumtime, Self等同于tottime。第二部分和第三部分都有很多标签,不同的标签标示从不同的角度来看结果,如图上所以,第三部分的“call graph”展示了该函数的call tree并包含每个子函数的时间百分比,一目了然。

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号