虽然不是所有的Python程序都需要严格的性能分析,不过知道如何利用Python生态圈里的工具来分析性能,也是不错的。

分析一个程序的性能,总结下来就是要回答4个问题:

  1. 它运行的有多快?

  2. 它的瓶颈在哪?

  3. 它占用了多少内存?

  4. 哪里有内存泄漏?

接下来,我们会着手使用一些很棒的工具,来帮我们回答这些问题。

粗粒度的计算时间

我们先来用个很快的方法来给我们的代码计时:使用unix的一个很好的功能 time。

1
2
3
4
5
$
time
python yourprogram.py
real 0m1.028s
user 0m0.001s
sys 0m0.003s

关于这3个测量值的具体含义可以看,但是简要的说就是:

  • real:代表实际花费的时间

  • user::代表cpu花费在内核外的时间

  • sys:代表cpu花费在内核以内的时间

通过把sys和user时间加起来可以获得cpu在你的程序上花费的时间。

如果sys和user加起来的时间比real时间要小很多,那么你可以猜想你的程序的大部分性能瓶颈应该是IO等待的问题。

用上下文管理器来细粒度的测量时间

我接下来要使用的技术就是让你的代码仪器化以让你获得细粒度的时间信息。这里是一个计时方法的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import
time
class
Timer(
object
):
def
__init__(
self
, verbose
=
False
):
self
.verbose
=
verbose
def
__enter__(
self
):
self
.start
=
time.time()
return
self
def
__exit__(
self
,
*
args):
self
.end
=
time.time()
self
.secs
=
self
.end
-
self
.start
self
.msecs
=
self
.secs
*
1000
# millisecs
if
self
.verbose:
print
'elapsed time: %f ms'
%
self
.msecs

为了使用它,将你想要测量时间的代码用Python关键字with和Timer上下文管理器包起来。它会在你的代码运行的时候开始计时,并且在执行结束的完成计时。

下面是一个使用它的代码片段:

1
2
3
4
5
6
7
8
9
10
11
from
timer
import
Timer
from
redis
import
Redis
rdb
=
Redis()
with Timer() as t:
rdb.lpush(
"foo"
,
"bar"
)
print
"=> elasped lpush: %s s"
%
t.secs
with Timer as t:
rdb.lpop(
"foo"
)
print
"=> elasped lpop: %s s"
%
t.secs

我会经常把这些计时器的输入记录进一个日志文件来让我知道程序的性能情况。

用分析器一行一行地计时和记录执行频率

Robert Kern有一个很棒的项目名叫 。我经常会用它来测量我的脚本里每一行代码运行的有多快和运行频率。

为了用它,你需要通过pip来安装这个Python包:

1
$ pip install line_profiler

在你安装好这个模块之后,你就可以使用line_profiler模块和一个可执行脚本kernprof.py。

为了用这个工具,首先需要修改你的代码,在你想测量的函数上使用@profiler装饰器。不要担心,为了用这个装饰器你不需要导入任何其他的东西。Kernprof.py这个脚本可以在你的脚本运行的时候注入它的运行时。

Primes.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@profile
def
primes(n):
if
n
=
=
2
:
return
[
2
]
elif
n<
2
:
return
[]
s
=
range
(
3
,n
+
1
,
2
)
mroot
=
n
*
*
0.5
half
=
(n
+
1
)
/
2
-
1
i
=
0
m
=
3
while
m <
=
mroot:
if
s[i]:
j
=
(m
*
m
-
3
)
/
2
s[j]
=
0
while
j<half:
s[j]
=
0
j
+
=
m
i
=
i
+
1
m
=
2
*
i
+
3
return
[
2
]
+
[x
for
x
in
s
if
x]
primes(
100
)
.

一旦你在你的代码里使用了@profile装饰器,你就要用kernprof.py来运行你的脚本:

1
$ kernprof.py -l -
v
fib.py

-l这个选项是告诉kernprof将@profile装饰器注入到你的脚本的内建里,-v是告诉kernprof在脚本执行完之后立马显示计时信息。下面是运行测试脚本后得到的输出:

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
Wrote profile results to primes.py.lprof
Timer unit:
1e
-
06
s
File
: primes.py
Function: primes at line
2
Total time:
0.00019
s
Line
# Hits Time Per Hit % Time Line Contents
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
2
@profile
3
def
primes(n):
4
1
2
2.0
1.1
if
n
=
=
2
:
5
return
[
2
]
6
1
1
1.0
0.5
elif
n<
2
:
7
return
[]
8
1
4
4.0
2.1
s
=
range
(
3
,n
+
1
,
2
)
9
1
10
10.0
5.3
mroot
=
n
*
*
0.5
10
1
2
2.0
1.1
half
=
(n
+
1
)
/
2
-
1
11
1
1
1.0
0.5
i
=
0
12
1
1
1.0
0.5
m
=
3
13
5
7
1.4
3.7
while
m <
=
mroot:
14
4
4
1.0
2.1
if
s[i]:
15
3
4
1.3
2.1
j
=
(m
*
m
-
3
)
/
2
16
3
4
1.3
2.1
s[j]
=
0
17
31
31
1.0
16.3
while
j<half:
18
28
28
1.0
14.7
s[j]
=
0
19
28
29
1.0
15.3
j
+
=
m
20
4
4
1.0
2.1
i
=
i
+
1
21
4
4
1.0
2.1
m
=
2
*
i
+
3
22
50
54
1.1
28.4
return
[
2
]
+
[x
for
x
in
s
if
x]
.

在里面寻找花费时间比较长的行,有些地方在优化之后能带来极大的改进。

它用了多少内存?

现在,我们已经能很好的测量代码运行时间了,接下来就是分析代码用了多少内存了。幸运的是,Fabian Pedregosa已经完成了一个很好的,它模仿了Robert Kern的line_profile。

首先,用pip来安装它:

1
2
$ pip install
-
U memory_profiler
$ pip install psutil

(推荐安装psutils包,这是因为这能大大提升memory_profiler的性能)

跟line_profiler类似,memory_profiler需要用@profiler装饰器来装饰你感兴趣的函数,就像这样:

1
2
3
4
@profile
def
primes(n):
...
...

用一下的命令来查看你的函数在运行时耗费的内存:

1
$ python
-
m memory_profiler primes.py

在代码运行完之后,你就应该能看到一下的输出:

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
Filename: primes.py
Line
# Mem usage Increment Line Contents
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
2
@profile
3
7.9219
MB
0.0000
MB
def
primes(n):
4
7.9219
MB
0.0000
MB
if
n
=
=
2
:
5
return
[
2
]
6
7.9219
MB
0.0000
MB
elif
n<
2
:
7
return
[]
8
7.9219
MB
0.0000
MB s
=
range
(
3
,n
+
1
,
2
)
9
7.9258
MB
0.0039
MB mroot
=
n
*
*
0.5
10
7.9258
MB
0.0000
MB half
=
(n
+
1
)
/
2
-
1
11
7.9258
MB
0.0000
MB i
=
0
12
7.9258
MB
0.0000
MB m
=
3
13
7.9297
MB
0.0039
MB
while
m <
=
mroot:
14
7.9297
MB
0.0000
MB
if
s[i]:
15
7.9297
MB
0.0000
MB j
=
(m
*
m
-
3
)
/
2
16
7.9258
MB
-
0.0039
MB s[j]
=
0
17
7.9297
MB
0.0039
MB
while
j<half:
18
7.9297
MB
0.0000
MB s[j]
=
0
19
7.9297
MB
0.0000
MB j
+
=
m
20
7.9297
MB
0.0000
MB i
=
i
+
1
21
7.9297
MB
0.0000
MB m
=
2
*
i
+
3
22
7.9297
MB
0.0000
MB
return
[
2
]
+
[x
for
x
in
s
if
x]
.

本文由 伯乐在线 - 贱圣OMG 翻译自 Huy Nguyen。欢迎加入技术翻译小组。转载请参见文章末尾处的要求。

【感谢 贱圣OMG 的热心翻译。如果其他朋友也有不错的原创或译文,可以提交到伯乐在线。】