目录
你的瓶颈在哪里?CPU时间 vs 挂钟时间
如果你的进程运行缓慢,可能是因为它非常占用CPU——或者可能是因为它在等待I/O(网络或文件系统)、锁,或者只是在休眠。但如何判断呢?
有多种方法可以回答这个问题,但在本文中,我将介绍可能是最简单的启发式方法:比较CPU时间和挂钟时间。
我将首先展示如何在整个进程运行期间测量它,然后演示如何使用Python随时间测量它。
挂钟时间 vs CPU时间
挂钟时间测量的是实际经过的时间,就像你看着墙上的时钟一样。CPU时间是CPU忙碌的秒数。
为了理解性能,你需要比较这两者。例如,host
命令会向DNS服务器发送查询,以找出我的域名使用的邮件服务器:
$ host -t MX pythonspeed.com
pythonspeed.com mail is handled by in2-smtp.messagingengine.com.
pythonspeed.com mail is handled by in1-smtp.messagingengine.com.
这个命令在哪里花费了时间?
我们可以使用Unix系统上的time
命令来大致了解:
$ time host -t MX pythonspeed.com
pythonspeed.com mail is handled by in1-smtp.messagingengine.com.
pythonspeed.com mail is handled by in2-smtp.messagingengine.com.
real 0m0.069s
user 0m0.006s
sys 0m0.005s
time
命令运行传递给它的参数,并记录三行额外的输出:
real
:挂钟时间。user
:进程的CPU时间。sys
:由于进程的系统调用而导致的操作系统CPU时间。
在这个例子中,挂钟时间比CPU时间要长,这表明进程花费了大量时间等待(大约58毫秒),而不是一直在进行计算。它在等待什么?可能是在等待我的ISP的DNS服务器的网络响应。
换句话说,host
命令的瓶颈不是CPU,而是网络。 即使我购买了一台CPU速度翻倍的计算机,对返回结果的时间影响也非常小。
重要提示: 如果机器上运行了许多其他进程,这些进程会占用一些CPU。如果它们非常繁忙,你正在基准测试的进程可能最终会等待CPU空闲。
目前只需记住,如果你在测量性能,应该尽量在一台没有其他进程运行的安静机器上进行。如果你正在进行严格的基准测试,可能会对在噪声中甚至不同CPU模型下进行可靠基准测试的技术感兴趣。
从不同的比率中可以学到什么
在上面的例子中,CPU时间比挂钟时间要短,但也可能存在其他关系。将可能的关系表示为(CPU时间) / (挂钟时间)
的比率更容易理解,即CPU/秒
。
如果这是一个单线程进程:
CPU/秒 ≈ 1
: 进程花费了所有时间使用CPU。更快的CPU可能会使程序运行得更快。CPU/秒 < 1
: 数值越低,进程花费在等待(网络、硬盘、锁、其他进程释放CPU,或者只是休眠)的时间越多。例如,如果CPU/秒是0.75,那么25%的时间花费在等待上。
如果这是一个多线程进程,并且你的计算机有N
个CPU和至少N
个线程,CPU/秒
可以高达N
。
CPU/秒 < 1
: 进程花费了大量时间等待。CPU/秒 ≈ N
: 进程饱和了所有的CPU。- 其他值: 进程使用了等待和CPU的某种组合,仅凭此测量结果很难判断瓶颈是什么。你可以通过线程数量来获得一些线索——如果你运行了2个线程并且
CPU/秒 = 2
,你知道你已经饱和了。
使用psutil Python库随时间测量CPU使用情况
到目前为止,我们一直在查看整个进程运行期间的CPU使用情况,但实际上我们可以随时间测量它。在Python中,使用psutil
库可以轻松实现这一点,它可以让你获取有关进程的许多有用信息。
你可以查看系统时间、用户时间,以及子进程的相应时间:
>>> import psutil
>>> p = psutil.Process(pid=6045)
>>> p.cpu_times()
pcputimes(user=345.45, system=12.8, children_user=220.03, children_system=17.37)
为了查看随时间的变化,我们可以编写一个小工具,每秒打印出任意进程及其子进程的CPU/秒
:
import sys
import time
import psutil
process = psutil.Process(int(sys.argv[1]))
last_times = process.cpu_times()
while process.is_running():
time.sleep(1)
times = process.cpu_times()
usage = sum(times) - sum(last_times)
last_times = times
print("{:.2f} CPU/sec".format(usage))
我们可以使用它来查看PID为6045的进程使用了多少CPU:
$ python cpu-over-time.py 6045
0.03 CPU/sec
0.09 CPU/sec
0.12 CPU/sec
0.40 CPU/sec
0.50 CPU/sec
0.13 CPU/sec
最窄的瓶颈是需要修复的
如果90%的进程时间花费在等待网络上,那么专注于CPU处理优化通常没有意义。因此,性能优化的第一步始终是识别瓶颈。
根据你构建的内容,瓶颈通常不是CPU。