Python性能分析基础

  |  

摘要: 《Python性能分析与优化》笔记 part1

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


1. 什么是性能分析

性能分析就是分析代码和它正在使用的资源之间有着怎样的关系。

性能分析软件有两类方法论:
(1)基于事件的性能分析(event-based profiling)
(2)统计式性能分析(statistical profiling)

Event-based profiling

不是所有的编程语言都支持这类性能分析。支持这类基于事件的性能分析的编程语言主要有以下几种:

  • java: JVMTI(JVM Tools Interface,JVM工具接口)为性能分析器提供了钩子,可以跟踪诸如函数调用、线程相关的事件、类加载之类的事件。
  • python:开发者可以用 sys.setprofile 函数,跟踪 python_[call|return|exception] 或 c_[call|return|exception] 之类的事件。

基于事件的性能分析器(event-based profiler,也称为轨迹性能分析器,tracing profiler)是通过收集程序执行过程中的具体事件进行工作的。
需要监听的事件越多,产生的数据量就越大。这导致它们不太实用。
当其他性能分析方法不够用或者不够精确时,它们可以作为最后的选择。

基于事件的性能分析器的例子

sys.setprofile(profiler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import profile
import sys

def profiler(frame, event, arg):
print("PROFILER: {} {}".format(event, arg))

sys.setprofile(profiler)

def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)

def fib_seq(n):
seq = []
if n > 0:
seq.extend(fib_seq(n - 1))
seq.append(fib(n))
return seq

print(fib_seq(2))

程序输出

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
30
PROFILER: call None
PROFILER: call None
PROFILER: call None
PROFILER: call None
PROFILER: return 0
PROFILER: c_call <built-in method append of list object at 0x7f3ac2ccb408>
PROFILER: c_return <built-in method append of list object at 0x7f3ac2ccb408>
PROFILER: return [0]
PROFILER: c_call <built-in method extend of list object at 0x7f3ac2cc68c8>
PROFILER: c_return <built-in method extend of list object at 0x7f3ac2cc68c8>
PROFILER: call None
PROFILER: return 1
PROFILER: c_call <built-in method append of list object at 0x7f3ac2cc68c8>
PROFILER: c_return <built-in method append of list object at 0x7f3ac2cc68c8>
PROFILER: return [0, 1]
PROFILER: c_call <built-in method extend of list object at 0x7f3ac2c719c8>
PROFILER: c_return <built-in method extend of list object at 0x7f3ac2c719c8>
PROFILER: call None
PROFILER: call None
PROFILER: return 1
PROFILER: call None
PROFILER: return 0
PROFILER: return 1
PROFILER: c_call <built-in method append of list object at 0x7f3ac2c719c8>
PROFILER: c_return <built-in method append of list object at 0x7f3ac2c719c8>
PROFILER: return [0, 1, 1]
PROFILER: c_call <built-in function print>
[0, 1, 1]
PROFILER: c_return <built-in function print>
PROFILER: return None

可以发现: PROFILER会被每一个事件调用

这个工具输出的信息过多,因此性能分析时是作为最后的选择使用的。

Statistical profiling

统计式性能分析器以固定的时间间隔对程序计数器(program counter)进行抽样统计。

优点:

  1. 分析的数据更少:由于我们只对程序执行过程进行抽样,而不用保留每一条数据,因此需要分析的信息量会显著减少。
  2. 对性能造成的影响更小:由于使用抽样的方式(用操作系统中断),目标程序的性能遭受的干扰更小。虽然使用性能分析器并不能做到100%无干扰,但是统计式性能分析器比基于事件的性能分析器造成的干扰要小。

2. 性能分析可以分析什么

测量是性能分析的核心。

运行时间(Execution Time)

做性能分析时,我们能够收集到的最基本的数值就是运行时间。整个进程或代码中某个片段的运行时间会暴露相应的性能。

依然看前面的计算斐波那契数列前 n 位的例子。只需要把几行代码加入程序运行就可以获得运行时间了。

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
30
31
32
33
import datetime

t_start = None
t_end = None

def start_time():
global t_start
t_start = datetime.datetime.now()

def get_delta():
global t_start
t_end = datetime.datetime.now()
return t_end - t_start

start_time()
print("About to calculate the fibonacci sequence for the number 30")
delta1 = get_delta()

start_time()
seq = fib_seq(30)
delta2 = get_delta()

print("Now we print the numbers: ")
start_time()
for n in seq:
print(n)
delta3 = get_delta()

print("=====Profiling Results=====")
print("Time required to print a simple message: {}s".format(delta1))
print("Time required to calculate fibonacci: {}s".format(delta2))
print("Time required to iterate and print the numbers: {}s".format(delta3))
print("==========")

输出:

1
2
3
4
5
=====Profiling Results=====
Time required to print a simple message: 0:00:00.000042s
Time required to calculate fibonacci: 0:00:00.657711s
Time required to iterate and print the numbers: 0:00:00.000074s
==========

我们可以看到,最费时的是中间的计算部分。

瓶颈定位

只要测量出了程序的运行时间,就可以把注意力移到运行慢的环节上做性能分析。通常,瓶颈都是由下面的一种或几种原因造成的。

  1. 沉重的I/O操作,比如读取和分析大文件,长时间执行数据库查询,调用外部服务(比如HTTP请求),等等。
  2. 出现了内存泄漏,消耗了所有的内存,导致后面的程序没有内存来正常执行。
  3. 未经优化的代码被频繁地执行。
  4. 密集的操作在可以缓存时没有缓存,占用了大量资源。

I/O关联的代码(文件读/写、数据库查询等)很难优化,因为优化有可能会改变程序执行I/O操作的方式(通常是语言的核心函数操作I/O)。
在性能优化接近尾声的时候,剩下的大多数性能瓶颈都是由I/O关联的代码造成的。

$3 内存消耗与内存泄露

随着RAM和高级编程语言都开始支持自动内存管理(比如垃圾回收机制),开发者不需要关注内存优化了,系统会帮忙完成的。

如果我们把一个调用大量外部资源的程序的内存消耗随时间的变化描绘出来,可能如下图所示。

资源加载时,内存使用曲线出现高峰;资源释放时,曲线会下降。

可以统计没有加载资源时程序的内存消耗的平均值。只要确定了这个平均值(图中用矩形表示),就可以判断内存泄漏的情况。

在上图中你会发现,每当资源不再使用时,占用的内存并没有完全释放,这时内存消耗曲线就会位于矩形之上。这就表示程序会消耗越来越多的内存,即使加载资源已经释放也是如此

$4 性能优化的实践

如果你还没有对代码做过测量(性能分析),优化往往不是个好主意。首先,应该集中精力完成代码,然后通过性能分析发现真正的性能瓶颈,最后对代码进行优化。

性能分析是重复性的工作。为了获得最佳性能,你可能需要在一个项目中做很多次性能分析,在另一个项目里还要再做一次。

Build a regression-test suite 建立回归测试套件

在进行性能优化时,需要保证不管代码怎么变化,功能都不会变糟。最好的做法,尤其是面对大型项目时,就是建立测试套件。确保代码具有足够的覆盖率,可以让你信心去优化。覆盖率只有60%的测试套件在优化时可能会导致严重后果。

回归测试套件可以保证你在代码中尝试任何优化时,都不用担心代码的结构被破坏。

思考代码结构

函数代码之所以容易进行重构(refactor),是因为这种代码结构没有副作用。这样可以降低改变系统中其他部分的风险。如果你的代码没有局部可变的状态,将是另一个优势。

耐心

大多数情况下,你遇到的问题都不是很容易解决的。这就表明你必须浏览数据,描绘图形以便理解,不断地缩小检测范围,直到你重新开启新一轮分析,或者最终找到问题所在。

值得注意的是,对数据分析得越深入,表明你陷入的坑越深,数据将无法指明正确的优化方向,因此要时刻清楚自己的目标,并且在你开始之前已准备好正确的工具。

尽可能多地收集数据

根据软件的不同类型和规模,在分析之前,你可能需要获取尽量多的数据。性能分析器很适合做这件事。但是,还有其他数据资源,如网络应用的系统日志、自定义日志、系统资源快照(如操作系统任务管理器),等等。

数据预处理

当你拥有了性能分析器的信息、日志和其他资源之后,在分析之前可能需要对数据进行预处理(ETL)。不要因为性能分析器不能理解就回避非结构化数据。

数据可视化

如果在错误发生之前,你不清楚自己要找的问题,只是想知道优化代码 的方式,那么洞察你已经预处理过的数据的最好方式就是数据可视化。


Share