Python代码中的捕捉性能-CPU分析(解释器)

时间:2022-10-20 17:06:50

在这篇文章中,我将介绍一些工具和方法,可以在运行Python脚本时对剖析器进行剖析。

就像在我们之前的文章中一样,CPU分析的含义是一样的,但是现在我们不是针对Python脚本的。相反,我们想知道Python解释器是如何工作的,以及在运行我们的Python脚本时花费多少时间。

接下来我们将看到如何跟踪CPU使用情况,并找到解释器中的热点。

系列索引

一旦帖子发布,下面的链接将会生效:

  1. 建立
  2. 内存分析
  3. CPU分析 - Python脚本
  4. CPU分析 - Python解释器

测量CPU使用率

对于这篇文章,我将主要使用与内存分析和脚本CPU使用相同的脚本,您可以在下面或在这里看到它

import time


def primes(n):
if n == 2:
return [2]
elif n < 2:
return []
s = []
for i in range(3, n+1):
if i % 2 != 0:
s.append(i)
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
l = [2]
for x in s:
if x:
l.append(x)
return l


def benchmark():
start = time.time()
for _ in xrange(40):
count = len(primes(1000000))
end = time.time()
print "Benchmark duration: %r seconds" % (end-start)


benchmark()


优化后的版本是bellow或here:

import time


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]


def benchmark():
start = time.time()
for _ in xrange(40):
count = len(primes(1000000))
end = time.time()
print "Benchmark duration: %r seconds" % (end-start)


benchmark()


CPython

CPython是多功能的,完全用C编写,因此更容易测量和/或配置文件。您可以在这里找到在GitHub上托管的CPython的源代码默认情况下,你会看到最新的分支,在写这个的时候是3.7+,但是所有其他的分支都可以下到2.7。

在我们的帖子中,我们将重点介绍CPython 2,但同样的步骤可以成功应用到最新的3版本。

代码覆盖工具

查看正在运行的C代码部分的最简单方法之一是使用代码覆盖率工具。

我们首先克隆回购:

git clone  https://github.com/python/cpython / 
cd cpython 
git checkout 2.7 
./configure

复制目录中的脚本并运行以下命令:

make coverage
./python 04.primes-v1.py
make coverage-lcov

第一行将编译GCOV支持的解释器,第二行将运行工作负载并收集.gcda文件中的分析数据,第三行解析包含分析数据的文件,并在名为的文件夹中创建一堆HTML文件lcov-report

如果我们index.html在浏览器中打开,我们可以看到解释器源代码中被执行的位置来运行我们的Python脚本。你会看到像下面的东西:

Python代码中的捕捉性能-CPU分析(解释器)

在顶层,我们可以看到构成源代码的每个目录以及覆盖的代码量。例如,我们打开Objects 目录,  listobject.c.gcov.html文件。虽然我们不会完全解读,但我们分析一部分。看下面的部分。

Python代码中的捕捉性能-CPU分析(解释器)


如何阅读?在黄色的列上,你可以看到来自C文件代码的行号。在下一列中,您可以看到特定行被执行的次数。在最右边的列中,您可以看到实际的C源代码。

在这个例子中,这个方法listiter_next被称为6000万次。

我们是如何得到这个功能的?如果我们仔细看看我们的Python脚本,我们观察到它使用了很多列表迭代并追加。(另一点可以导致脚本优化首先

让我们继续其他一些专用工具。

PERF

有关更多信息,我们可以使用perfLinux系统上提供工具。官方文档可以在这里阅读

我们(重新)使用以下内容构建CPython解释器。如果你没有在同一个目录中下载Python脚本,你应该这样做。另外,请确保perf 已安装在您的系统上。

make clean
./configure --with-pydebug
make

运行perf 如下。Brendan Gregg写的这个优秀的页面可以看到更多的使用perf的方法

sudo perf record ./python 04.primes-v1.py

运行脚本之后,您将看到如下所示的内容:

Benchmark duration: 32.03910684585571 seconds
[21868 refs]
 perf record: Woken up 20 times to write data ]
[ perf record: Captured and wrote 4.914 MB perf.data (125364 samples) ]

要查看结果,运行`sudo ​perf report` 以获得度量。

Python代码中的捕捉性能-CPU分析(解释器)

只有最有趣的电话才能被保存。在上面的屏幕中,我们看到花费的时间最多PyEval_EvalFrameEx这是主要的解释器循环,我们对这个例子不感兴趣。相反,我们感兴趣的是下一个耗时的功能 -   listiter_next占用10.70%的时间。

运行优化版本后,我们看到以下内容:

Python代码中的捕捉性能-CPU分析(解释器)


经过我们的优化,该listiter_next 功能只消耗了2.11%的时间。读者可以根据口译员的情况做进一步的优化。

Valgrind的/ Callgrind

另一个可以用来查找瓶颈的工具是Valgrind,它的一个叫做callgrind的插件。更多细节可以在这里阅读

如果尚未完成,我们(重新)使用以下内容构建CPython解释器。如果你没有在同一个目录中下载Python脚本,你应该这样做。另外,请确保valgrind 已安装在您的系统上。

make clean 
./configure --with-pydebug 
make

运行valgrind 如下:

valgrind --tool=callgrind --dump-instr=yes \
--collect-jumps=yes --collect-systime=yes \
--callgrind-out-file=callgrind-%p.out -- ./python 04.primes-v1.py

结果是:

Benchmark duration: 1109.4096319675446 seconds
[21868 refs]
==24152== 
==24152== Events : Ir sysCount sysTime
==24152== Collected : 115949791666 942 208
==24152== 
==24152== I refs: 115,949,791,666

为了可视化,我们将使用KCacheGrind

kcachegrind callgrind-2327.out

PyPy

在PyPy上,可以成功使用的配置文件是非常有限的,恢复到vmprof,一个由开发PyPy的人写的工具。

首先,从这里下载PyPy 在此之后,启用pip 对它的支持

bin/pypy -m ensurepip

安装vmprof很简单,只需运行:

bin/pypy -m pip install vmprof

运行工作量为:

bin/pypy -m vmprof --web 04.primes-v1.py


并在浏览器中打开控制台中显示的链接(以http://vmprof.com/#/开头  




作者:Alecsandru Patrascu,alecsandru.patrascu [at] rinftech [dot] com

原文:https://pythonfiles.wordpress.com/2017/08/24/hunting-performance-in-python-code-part-4/