Python优化第一步: 性能分析实践

简介:

先扔上一句名言来镇楼。

当我们的确是有需要开始真正优化我们的Python程序的时候,我们要做的第一步并不是盲目的去做优化,而是对我们现有的程序进行分析,发现程序的性能瓶颈进而进行针对性的优化。这样才会使我们花时间和精力去做的优化获得最大的效果。

正文

关于性能分析

性能分析就是分析代码和正在使用的资源之间有着怎样的联系,它可以帮助我们分析运行时间从而找到程序运行的瓶颈,也可以帮助我们分析内存的使用防止内存泄漏的发生。

帮助我们进行性能分析的工具便是性能分析器,它主要分为两类:

8481c8f592b7f349aa84a1de5c171db681516edf基于事件的性能分析(event-based profiling)
8481c8f592b7f349aa84a1de5c171db681516edf 统计式的性能分析(statistical profiling)

关于性能分析详细的概念参考: 性能分析-维基百科

Python的性能分析器

Python中最常用的性能分析工具主要有:cProfiler, line_profiler以及memory_profiler等。他们以不同的方式帮助我们分析Python代码的性能。我们这里主要关注Python内置的cProfiler,并使用它帮助我们分析并优化程序。

cProfiler

快速使用

这里我先拿上官方文档的一个简单例子来对cProfiler的简单使用进行简单介绍。

 

1

2

3

 

import cProfile

import re

cProfile.run('re.compile("foo|bar")')

分析结果:

 

1

2

3

4

5

6

7

8

9

10

11

12

 

197 function calls (192 primitive calls) in 0.002 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)

1 0.000 0.000 0.001 0.001 <string>:1(<module>)

1 0.000 0.000 0.001 0.001 re.py:212(compile)

1 0.000 0.000 0.001 0.001 re.py:268(_compile)

1 0.000 0.000 0.000 0.000 sre_compile.py:172(_compile_charset)

1 0.000 0.000 0.000 0.000 sre_compile.py:201(_optimize_charset)

4 0.000 0.000 0.000 0.000 sre_compile.py:25(_identityfunction)

3/1 0.000 0.000 0.000 0.000 sre_compile.py:33(_compile)

从分析报告结果中我们可以得到很多信息:

8481c8f592b7f349aa84a1de5c171db681516edf 整个过程一共有197个函数调用被监控,其中192个是原生调用(即不涉及递归调用)
8481c8f592b7f349aa84a1de5c171db681516edf 总共执行的时间为0.002秒
8481c8f592b7f349aa84a1de5c171db681516edf 结果列表中是按照标准名称进行排序,也就是按照字符串的打印方式(数字也当作字符串)
8481c8f592b7f349aa84a1de5c171db681516edf 在列表中:
  • ncalls表示函数调用的次数(有两个数值表示有递归调用,总调用次数/原生调用次数)

  • tottime是函数内部调用时间(不包括他自己调用的其他函数的时间)

  • percall等于 tottime/ncalls

  • cumtime累积调用时间,与tottime相反,它包含了自己内部调用函数的时间

  • 最后一列,文件名,行号,函数名

优雅的使用

Python给我们提供了很多接口方便我们能够灵活的进行性能分析,其中主要包含两个类cProfile模块的Profile类和pstat模块的Stats类。

我们可以通过这两个类来将代码分析的功能进行封装以便在项目的其他地方能够灵活重复的使用进行分析。

这里还是需要对Profile以及Stats的几个常用接口进行简单总结:

8481c8f592b7f349aa84a1de5c171db681516edfProfile 类:
    • enable(): 开始收集性能分析数据

    • disable(): 停止收集性能分析数据

    • create_stats(): 停止收集分析数据,并为已收集的数据创建stats对象

    • print_stats(): 创建stats对象并打印分析结果

    • dump_stats(filename): 把当前性能分析的结果写入文件(二进制格式)

    • runcall(func, *args, **kwargs): 收集被调用函数func的性能分析数据

8481c8f592b7f349aa84a1de5c171db681516edfStats
pstats模块提供的Stats类可以帮助我们读取和操作stats文件(二进制格式)

 

1

2

 

import pstats

p = pstats.Stats('stats.prof')

Stats类可以接受stats文件名,也可以直接接受cProfile.Profile对象作为数据源。

    • strip_dirs(): 删除报告中所有函数文件名的路径信息

    • dump_stats(filename): 把stats中的分析数据写入文件(效果同cProfile.Profile.dump_stats())

    • sort_stats(*keys): 对报告列表进行排序,函数会依次按照传入的参数排序,关键词包括calls,cumtime等,具体参数参见https://docs.python.org/2/library/profile.html#pstats.Stats.sort_stats

    • reverse_order(): 逆反当前的排序

    • print_stats(*restrictions): 把信息打印到标准输出。*restrictions用于控制打印结果的形式, 例如(10, 1.0, ".*.py.*")表示打印所有py文件的信息的前10行结果。

有了上面的接口我们便可以更优雅的去使用分析器来分析我们的程序,例如可以通过写一个带有参数的装饰器,这样想分析项目中任何一个函数,便可方便的使用装饰器来达到目的。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

 

import cProfile

import pstats

import os

# 性能分析装饰器定义

def do_cprofile(filename):

"""

Decorator for function profiling.

"""

def wrapper(func):

def profiled_func(*args, **kwargs):

# Flag for do profiling or not.

DO_PROF = os.getenv("PROFILING")

if DO_PROF:

profile = cProfile.Profile()

profile.enable()

result = func(*args, **kwargs)

profile.disable()

# Sort stat by internal time.

sortby = "tottime"

ps = pstats.Stats(profile).sort_stats(sortby)

ps.dump_stats(filename)

else:

result = func(*args, **kwargs)

return result

return profiled_func

return wrapper

这样我们可以在我们想进行分析的地方进行性能分析, 例如我想分析我的MicroKineticModel类中的run方法:

 

1

2

3

4

5

6

7

 

class MicroKineticModel(km.KineticModel):

# ...

# 应用装饰器来分析函数

@do_cprofile("./mkm_run.prof")

def run(self, **kwargs):

# ...

装饰器函数中通过sys.getenv来获取环境变量判断是否需要进行分析,因此可以通过设置环境变量来告诉程序是否进行性能分析:

 

1

2

3

 

export PROFILING=y

# run the program...

程序跑完后便会在当前路径下生成mkm_run.prof的分析文件,我们便可以通过打印或者可视化工具来对这个函数进行分析。

性能分析实践

下面我就通过分析自己的动力学程序中MicroKineticModel类中的方法来进行实践,并使用常用的几种性能分析可视化工具来帮助分析并进行初步的优化和效率对比。

注: 本次测试的程序主要包含数值求解微分方程以及牛顿法求解多元非线性方程组的求解,其中程序中的公式推导部分全部通过字符串操作完成。

生成性能分析报告

按照上文的方法,我们通过装饰器对run方法进行修饰来进行性能分析,这样我们便可以像正常一样去跑程序,但是不同的是当前路径下会生成性能分析报告文件。

 

1

2

3

4

5

 

# 设置环境变量

export PROFILING=y

# 执行运行脚本

python run.py

在看似正常的运行之后,在当前路径下我们会生成一个分析报告, mkm_run.prof, 它是一个二进制文件,我们需要用python的pstats模块的接口来读取。

9acedb7f25bbae331c7449509e79c154c4e462c7

我们只按照累积时间进行降序排序并输出了前十行,整个函数只运行了0.106秒。可见程序大部分时间主要花在牛顿法求解的过程中,其中获取解析Jacobian Matrix的过程是一个主要耗时的部分。

虽然我们可以通过命令行查看函数调用关系,但是我并不想花时间在反人类的黑白框中继续分析程序,下面我打算上直观的可视化工具了。

分析数据可视化

gprof2dot

Gprof2Dot可将多种Profiler的数据转成Graphviz可处理的图像表述。配合dot命令,即可得到不同函数所消耗的时间分析图。具体使用方法详见: https://github.com/jrfonseca/gprof2dot

因此我们可以利用它来为我们的程序生成分析图:

 

1

 

gprof2dot -f pstats mkm_run.prof | dot -Tpng -o mkm_run.png

于是我们路径下面就生成了mkm_run.png

b999459b9e1e4fdbe7bd6143fc2e93e3d765410a

我倒是蛮喜欢这个时间分析图,顺着浅色方格的看下去很容易发现程序的瓶颈部分,

  • 每个node的信息如下:

     

    1

    2

    3

    4

    5

     

    +------------------------------+

    | function name |

    | total time % ( self time % ) |

    | total calls |

    +------------------------------+

  • 每个edge的信息如下:

     

    1

    2

    3

     

    total time %

    calls

    parent --------------------> children

vprof

也是一个不错的工具来提供交互式的分析数据可视化,详情参见: https://github.com/nvdv/vprof

他是针对文件进行执行并分析,并在浏览器中生成可视化图标

 

1

2

 

# 生成CPU flame图

vprof -c c run.py

dffd74db5f72bc111e4f3ae212e584c56c43798b
RunSnakeRun

RunSnakeRun是另一个可对性能分析结果进行可视化的工具,它使用wxPython讲Profiler的数据可视化

 

1

 

runsnake mkm_run.prof

效果图:

799e5babf22d48e56c2ae85cf8cd82fb9ea3a11a

KCacheGrind & pyprof2calltree

KCacheGrind是Linux中常用的分析数据可视化软件,他默认处理valgrind的输出,但是我们结合pyprof2calltree工具可以把cProfile的输出转换成KCacheGrind支持的格式。

 

1

 

pyprof2calltree -i mkm_run.prof -k # 转换格式并立即运行KCacheGrind

46234285ca4046afec57870dcc8605d373ad255e
初步优化

通过直观的可视化工具我们可以迅速找到程序中可以优化的部分,

44006953accc62bea2b54443ea1f1688b3b9cd78

可以看到我们在求解Jacobian矩阵的时候,会调用很多次求导函数,并且占据了比较大的时间,于是我们可以尝试通过函数返回值缓存的方式进行初步优化。

为了能将函数的返回值进行缓存,我们添加了一个描述符:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

 

class Memoized(object):

def __init__(self, func):

self.func = func

self.results = {}

def __get__(self, instance, cls):

self.instance = instance

return self

def __call__(self, *args):

key = args

try:

return self.results[key]

except KeyError:

self.results[key] = self.func(self.instance, *args)

return self.results[key]

这样,在我们需要进行返回值缓存的函数上面使用此描述符,便可以将返回值缓存到描述符对象中,当我们使用相同参数进行重复调用时候,便可以直接返回数值,复杂度将为O(1)。

 

1

2

3

 

@Memoized

def poly_adsorbate_derivation(self, adsorbate_name, poly_expression):

# ...

优化后我们再来进行一次分析:

b87ebd05051c77b68fc952390ba6596c3039a334

同一个函数,运行时间从0.106秒降到了0.061秒效率提升近了40%!

看一下函数调用关系图:

ec0288f3ed47253c6e616ce41140db8192019e0e

而且函数调用次数明显减少了,可以看到poly_adsorbate_derivation的调用次数从36次降到了9次,__total_term_adsorbate_derivation从192次降到了48次。

总结

本文对Python内置的性能分析器cProfile的使用进行了介绍,并以作者项目中的代码为例进行了实例分析和数据可视化,并使用了缓存的方式对Python程序进行了初步的优化,希望能借此帮助大家熟悉工具并分析自己Python程序性能的瓶颈写出更好更高效的Python程序。


原文发布时间为:2016-12-21

本文作者:Pytlab

本文来自云栖社区合作伙伴“Python中文社区”,了解相关信息可以关注“Python中文社区”微信公众号

相关文章
|
18天前
|
存储 程序员 开发者
Python编程基础:从入门到实践
【10月更文挑战第8天】在本文中,我们将一起探索Python编程的奇妙世界。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供有价值的信息。我们将从Python的基本概念开始,然后逐步深入到更复杂的主题,如数据结构、函数和类。最后,我们将通过一些实际的代码示例来巩固我们的知识。让我们一起开始这段Python编程之旅吧!
|
20天前
|
Web App开发 前端开发 JavaScript
探索Python科学计算的边界:利用Selenium进行Web应用性能测试与优化
【10月更文挑战第6天】随着互联网技术的发展,Web应用程序已经成为人们日常生活和工作中不可或缺的一部分。这些应用不仅需要提供丰富的功能,还必须具备良好的性能表现以保证用户体验。性能测试是确保Web应用能够快速响应用户请求并处理大量并发访问的关键步骤之一。本文将探讨如何使用Python结合Selenium来进行Web应用的性能测试,并通过实际代码示例展示如何识别瓶颈及优化应用。
69 5
|
5天前
|
数据可视化 数据挖掘 Python
使用Python进行数据可视化:探索与实践
【10月更文挑战第21天】本文旨在通过Python编程,介绍如何利用数据可视化技术来揭示数据背后的信息和趋势。我们将从基础的图表创建开始,逐步深入到高级可视化技巧,包括交互式图表和动态展示。文章将引导读者理解不同图表类型适用的场景,并教授如何使用流行的库如Matplotlib和Seaborn来制作美观且具有洞察力的可视化作品。
19 7
|
2天前
|
测试技术 开发者 Python
探索Python中的装饰器:从入门到实践
【10月更文挑战第24天】 在Python的世界里,装饰器是一个既神秘又强大的工具。它们就像是程序的“隐形斗篷”,能在不改变原有代码结构的情况下,增加新的功能。本篇文章将带你走进装饰器的世界,从基础概念出发,通过实际例子,逐步深入到装饰器的高级应用,让你的代码更加优雅和高效。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开一扇通往高效编程的大门。
|
9天前
|
调度 开发者 Python
探索Python中的异步编程:从基础到实践
在本文中,我们将深入探讨Python的异步编程世界。从asyncio库的基本概念出发,我们将逐步构建起对异步编程的理解,并探索如何在实际项目中应用这些技术。本文不仅涵盖了异步编程的基础知识,还提供了实用的代码示例,旨在帮助读者在Python中有效地使用异步编程,以提高应用程序的性能和响应能力。
|
15天前
|
测试技术 持续交付 Apache
性能怪兽来袭!Python+JMeter+Locust,让你的应用性能飙升🦖
【10月更文挑战第10天】随着互联网应用规模的不断扩大,性能测试变得至关重要。本文将探讨如何利用Python结合Apache JMeter和Locust,构建高效且可定制的性能测试框架。通过介绍JMeter和Locust的使用方法及Python的集成技巧,帮助应用在高负载下保持稳定运行。
55 2
|
15天前
|
机器学习/深度学习 数据挖掘 Serverless
手把手教你全面评估机器学习模型性能:从选择正确评价指标到使用Python与Scikit-learn进行实战演练的详细指南
【10月更文挑战第10天】评估机器学习模型性能是开发流程的关键,涉及准确性、可解释性、运行速度等多方面考量。不同任务(如分类、回归)采用不同评价指标,如准确率、F1分数、MSE等。示例代码展示了使用Scikit-learn库评估逻辑回归模型的过程,包括数据准备、模型训练、性能评估及交叉验证。
38 1
|
21天前
|
存储 数据处理 Python
深入解析Python中的生成器:效率与性能的双重提升
生成器不仅是Python中的一个高级特性,它们是构建高效、内存友好型应用程序的基石。本文将深入探讨生成器的内部机制,揭示它们如何通过惰性计算和迭代器协议提高数据处理的效率。
|
8天前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践###
【10月更文挑战第18天】 本文深入探讨了Python编程中设计模式的应用与实践,通过简洁明了的语言和生动的实例,揭示了设计模式在提升代码可维护性、可扩展性和重用性方面的关键作用。文章首先概述了设计模式的基本概念和重要性,随后详细解析了几种常用的设计模式,如单例模式、工厂模式、观察者模式等,在Python中的具体实现方式,并通过对比分析,展示了设计模式如何优化代码结构,增强系统的灵活性和健壮性。此外,文章还提供了实用的建议和最佳实践,帮助读者在实际项目中有效运用设计模式。 ###
10 0
|
14天前
|
人工智能 算法 搜索推荐
通义灵码在Python项目开发中的应用实践
通义灵码在Python项目开发中的应用实践
68 0