当前位置: 首页 > 后端技术 > Python

Python性能优化

时间:2023-03-26 19:08:46 Python

背景本文主要讨论python代码的性能分析和优化,旨在解决一些常见的语言层面的性能瓶颈。就是日常工作中的一些积累和总结,会比较基础和全面。顺便也会介绍一些服务架构方面的优化经验。一、Python语言背景Python简单易学,在数据计算和分析方面表现出色,催生了庞大的用户群体和活跃的社区,使其在数据分析、机器学习等领域具有先天优势。广泛的第三方支持,Python在在线服务上也被广泛使用。但是python在性能方面有着所有动态解释型高级语言的通病,也是制约python进一步广泛应用的重要因素。这也是这类解释型脚本语言的通病:2、从具体的业务应用场景来看,业务架构背景比较片面。下面我们结合我们目前的后端架构来看一下python性能瓶颈在业务应用中的具体表现。该系统是基于大数据和机器学习模型的在线风控系统。为大量金融机构提供风控服务。建模技术。这些也让我们基于python的服务性能面临着严峻的考验。下图是架构的简化图:代码优化的性能分析前提是知道性能瓶颈在哪里,程序运行的主要时间花在了哪里。一般可以在日志中统计运行时间。对于比较复杂的代码你也可以使用一些工具来定位。Python内置了丰富的性能分析工具,可以描述程序运行时的性能,并提供各种统计数据帮助用户定位程序的性能瓶颈。常用的profilers:cProfile、profile、line_profile、pprofile和hotshot等。当然pycharm等一些IDE也继承了perfectprofiling。这里只介绍几种有代表性的性能分析方法:1、使用装饰器实现函数的耗时统计。点装饰器通过闭包向原始函数添加新函数。Python可以使用装饰器作为语法糖。函数的耗时统计,但这仅限于一般的同步方式。在协程中,更一般的在生成器函数中,由于yield会释放当前线程,耗时统计会在执行到yield时中断返回。导致统计失败。下面是一个包含两层闭包的装饰器(因为需要给装饰器传递参数):deftime_consumer(module_name='public_module'):deftime_cost(func):#获取调用装饰器的函数路径filepath=sys._getframe(1).f_code.co_filename@wraps(func)defwrapper(*args,**kwargs):t1=time.time()res=func(*args,**kwargs)t2=time.time()content={}尝试:content['time_cost']=round(float(t2-t1),3)content['method']=func.__name__content['file']=filepath.split(os.sep)[-1]content['module']=module_namecontent_res=json.dumps(content)time_cost_logger.info(content_res)除了Exceptionase:time_cost_logger.warning('%sdetail:%s'%(str(e),traceback.format_exc()))returnresreturnwrapperreturntime_cost2.函数级性能分析工具cprofilecProfile从python2.5开始就是标准Python解释器的默认性能配置文件Analyzer,是一个确定性分析器,只测量CPU时间,不关心内存消耗等与内存相关的信息性能分析结果字段含义ncalls:函数被调用的次数。tottime:函数内部消耗的总时间。percall:每次通话花费的平均时间。cumtime:消费时间累计总和。filename:lineno(function):分析函数的文件名、行号、函数名。方法1.单个文件性能分析:python-mcProfile-stottimetest.py2.方法性能分析:importcProfiledeffunction():passif__name__=='__main__':cProfile.run("function()")3.项目中实时服务的性能分析:#一般需要绑定在服务框架的钩子函数中实现,下面两个方法分别放在入口钩子和出口钩子中;pstats格式化统计信息,并根据需要进行排序分析处理。def_start_profile(self):importcProfileself.pr=cProfile.Profile()self.pr.enable()def_print_profile_result(self):如果不是self.pr:returnself.pr.disable()importpstatsimportStringIOs=StringIO.StringIO()stats=pstats.Stats(self.pr,stream=s).strip_dirs().sort_stats('tottime')stats.print_stats(50)3.线级分析工具pprofile/line_profile使用line_profile和需要引入_kernprof__,所以这里选择pprofile。pprofile的效率虽然没有line_profile高,但是做性能分析的时候可以忽略。pprofile的用法与cprofile完全相同。性能分析结果字段含义Line:行号Hits:代码行执行次数Time:总执行时间Timeperhit:单次执行耗时%:耗时比例4.性能分析总结随便挑一个方便的易于使用的分析方法或工具。但总的思路是从整体到具体。例如,可以使用cprofile找出整个代码执行过程中耗时较长的函数,然后使用pprofile逐行分析这些函数,最终将代码的性能瓶颈精确定位到行级别。Python性能优化方法一、优化思路Python的性能优化方法有很多。具体问题可以从不同的角度去考虑和分析,但是可以归纳为两个思路:从服务架构和CPU效率的角度,将CPU密集型转为IO密集型优化。从代码执行和cpu利用率来看,需要提高代码性能和多核利用率。例如,基于此,python在线服务的优化思路可以从以下几个方面考虑:2、使用字典/集合等对哈希等数据结构的常见操作进行代码优化:检索、去重、交集、并集、差集1.在字典中/Searchincollection(后面代码中省略了计时部分)dic={str(k):1forkinxrange(1000000)}if'qth'indic.keys():passif'qth'indic:pass耗时:0.05900001525880.02,使用set求交集list1=list(range(10000))list2=[i*2foriinlist1]s1=set(list1)s2=set(list2)list3=[]#listIntersectionforkinlist1:ifkinlist2:list3.append(k)#time-consumedforsetintersections3=s1&s2:0.8199999332430.001000165939Ps:Set操作在去重方面表现突出,交集和差集:使用生成器代替可迭代对象,节省内存和计算资源。它不需要计算整个可迭代对象,只计算需要循环的部分。1.使用xrange代替range(python3中没有区别)foriinrange(1000000):passforiinxrange(1000000):pass耗时:0.08299994468690.03200006484992,listcomprehensionusinggeneratordic={str(k):1forkinxrange(100000)}list1=[kforkindic.keys()]list1=(kforkindic.keys())耗时:0.01300001144410.003000020980833,复杂逻辑生成的迭代对象使用函数而不是deffib(max)生成:n,a,b=0,0,1list=[]whilencPickle>eval。交换两个没有中间变量的变量的值(存在循环引用导致内存泄漏的风险)。不仅限于python内置函数,在某些情况下,内置函数的性能远不如自己写的。比如python的strptime方法会生成一个9位的时间祖先,往往需要根据这个祖先来计算时间戳,这种方法的性能很差。我们可以自己将时间字符串转换成split成需要的时间祖先。使用生成器重写直接返回列表的复杂函数,将简单函数替换为列表推导,但列表推导不能超过两个表达式。生成器>列表理解>映射/过滤器。关键代码可以依赖高性能的扩展包,所以有时候需要牺牲一些可移植性来换取性能;敢于尝试新版本的python。考虑优化成本,一般先优化数据结构和算法,提高时间/空间复杂度,比如使用集合、分而治之、贪心、动态规划等,最后再考虑架构和整体框架。Python代码的优化也需要具体问题具体分析,不局限于上述方法,只要能分析出性能瓶颈,问题就解决了一半。《约束理论与企业优化》指出:“抛开瓶颈,任何改善都是假象”。3.优化实例优化循环将不相关的代码带到循环的上层,去掉多余的循环。平均耗时从2.0239s提升到0.7896s,性能提升61%。Multi-process使用multi-process将与主进程无关的函数放到后台执行:链表被分片成多个进程执行:如图1秒内返回的请求比例提升了百分之十,性能提升了约200ms。但是不建议在代码中过多使用它,因为它会在业务高峰期对机器负载造成压力。用时间祖先替换strptime的方法如图所示。模块平均耗时从123ms提升到79ms,提升了35.7%,针对一些badcase的优化效果会更加明显:使用collection将复杂字典转化为md5hashablestring后,通过集合重复数据删除将性能提高了60%以上。数据量越大,优化效果越好。微服务业务解耦将特征计算作为分布式微服务,实现IO和计算解耦,将CPU密集型转为IO密集型。在框架和服务选择方面,我们测试了tornado协程、uwsgi多进程、导入代码库、celery分布式计算等方式,tornado在性能和可用性上有一定的优势,上层nginx作为代理进行端口转发以及负载均衡:ab压测前后的性能对比,虽然在单个请求上没有优势,但是对于高并发系统,并发量有明显提升:ab压测前后的性能对比,虽然单个请求没有优势,但是对于高并发系统,并发量有明显提升:耗时模块流水线实时计算:命中流水线实时特性后的性能提升:python的高性能服务框架虽然python的语言特性导致其在cpu密集型代码中的表现堪忧,但是python非常适合IO密集型网络应用,加上其出色的数据分析处理能力和广泛的第三方支持,python在服务框架中的应用也很广泛。比如Django、flask、Tornado,如果首先考虑性能,就一定要选择高性能的服务框架。Python的高性能服务基本上都是通过协程和基于epoll的事件循环实现的IO多路复用框架。Tornado依靠强大的ioloop事件循环和gen-encapsulated协程,让我们可以通过yield关键字同步编写异步代码。在python3.5+中,python引入了原生的异步网络库asyncio,并提供了原生的事件循环get_event_loop来支持协程。并使用async/await来更好的封装协程。在tornado6.0中,ioloop已经实现了asyncio事件循环的封装。除了标准库asyncio的事件循环,社区还使用Cython实现了另一个事件循环uvloop。用于替换标准库。号称性能最好的python异步IO库。前面提到,python的高性能服务实现是基于协程和事件循环的,所以我们可以尝试协程和事件循环的不同组合来改造tornado服务,以达到最佳性能。限于篇幅,这里就不详细展开了。我们可以简单的看下异步服务框架在python2和python3中的表现,发现在服务端的事件循环中,python3的优势很明显。而且在三方库的兼容性、其他异步性能库的支持、协程循环语法和关键字支持等方面,推荐使用python3。在更复杂的项目中,新版本的优势会更加明显。但是不管是新旧版本的python,协程+事件循环的效率都比多进程或者线程要高很多。顺便说一句,这里有一个支持协程的python3异步IO库,基本支持常见的中间件:https://github.com/aio-libs?p...