性能不仅仅与CPU有关:为Python创建自己的探查器

假设您的Python程序运行缓慢,并且您发现这部分原因是由于缺乏处理器资源 。 我如何找出代码的哪些部分被迫期望不适用于CPU?



阅读了我们今天发布的翻译材料后,您将学习如何编写自己的Python代码分析器。 我们正在谈论的工具将在等待某些资源释放时检测到非活动位置。 特别是,我们将在这里讨论以下内容:

  • 该计划可以期待什么?
  • 分析不是CPU资源的资源的使用。
  • 分析意外上下文切换。

该计划期望什么?


在程序不忙于使用处理器进行大量计算的那些时刻,它似乎在等待某些东西。 这是导致程序不动作的原因:

  • 网络资源。 这可能包括等待DNS查找完成,等待来自网络资源的响应,等待某些数据加载等等。
  • 硬碟 从硬盘驱动器读取数据可能需要一些时间。 关于写入磁盘也可以这样说。 有时,仅使用位于RAM中的缓存执行读取或写入操作。 使用这种方法,一切都会很快发生。 但是有时候,当程序直接与磁盘交互时,这种操作会变得很慢。
  • 锁 程序可能会等待解锁线程或进程。
  • 暂停工作。 有时程序可能会故意暂停工作,例如,在尝试执行某些操作之间暂停。

如何找到程序中发生严重影响性能的地方?

方法1:分析程序不使用处理器的时间


Python的内置探查器cProfile能够收集与程序操作相关的许多不同指标的数据。 因此,可以使用它来创建一个工具,您可以使用该工具分析程序不使用处理器资源的时间。

操作系统可以准确告诉我们程序使用了多少处理器时间。

想象一下,我们正在分析一个单线程程序。 多线程程序更难以分析,并且描述此过程也不容易。 如果程序运行了9秒钟,并且同时使用了处理器7.5秒钟,则意味着它花费了1.5秒钟的等待时间。

首先,创建一个计时器来测量超时:

 import os def not_cpu_time():    times = os.times()    return times.elapsed - (times.system + times.user) 

然后创建一个分析器来分析这次:

 import cProfile, pstats def profile_not_cpu_time(f, *args, **kwargs):    prof = cProfile.Profile(not_cpu_time)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

之后,您可以分析各种功能:

 >>> profile_not_cpu_time( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)    3  0.050  0.017 _ssl._SSLSocket.read    1  0.040  0.040 _socket.getaddrinfo    1  0.020  0.020 _socket.socket.connect    1  0.010  0.010 _ssl._SSLSocket.do_handshake  342  0.010  0.000 find.str  192  0.010  0.000 append.list 

结果使我们得出结论,大部分时间都花在了从套接字读取数据上,但是花了一些时间来执行DNS查找( getaddrinfo )以及执行TCP握手( connect )和TLS / SSL握手。

由于我们一直在仔细研究程序在不使用处理器资源的情况下的运行时间,因此我们知道所有这些都是纯等待时间,即程序不忙于任何计算的时间。

为什么要记录str.findlist.append ? 在执行此类操作时,程序没有什么可等待的,因此解释似乎是合理的,根据这种解释,我们正在处理未执行整个过程的情况。 也许-等待其他过程的完成,或者等待完成从交换文件将数据加载到内存中的过程。 这表明花了一些时间来执行这些操作,这不是处理器时间的一部分。

另外,我想指出的是,我看到了一些报告,其中包含很小的负面时间片段。 这意味着经过时间和处理器时间之间存在一定差异,但是我认为这不会对更复杂程序的分析产生重大影响。

方法2:分析有意上下文切换的数量


测量程序在等待某事上花费的时间的问题在于,当对同一程序执行不同的测量会话时,由于某些事情超出了程序的范围,它可能会有所不同。 有时DNS查询可能比平常慢。 有时,某些数据可能比平常更慢地加载。 因此,使用一些更可预测的指标将是有用的,这些指标与程序周围的速度无关。

一种方法是计算完成该过程需要等待的操作数。 也就是说,我们正在谈论的是计算等待时间,而不是等待时间。

进程可以停止使用处理器资源,其原因有两个:

  1. 每次进程执行的操作不会立即结束时,例如,它从套接字读取数据,暂停等等,这等同于它对操作系统说的内容:“当我可以继续工作时叫醒我。” 这就是所谓的“故意上下文切换”:处理器可以切换到另一个进程,直到数据出现在套接字上,或者直到我们的进程退出待机模式为止,以及在其他类似情况下。
  2. “意外上下文切换”是操作系统暂时停止一个进程,从而允许另一个进程利用处理器资源的情况。

我们将介绍有意的上下文切换。

让我们写一个分析器,使用psutil库来计算故意的上下文切换:

 import psutil _current_process = psutil.Process() def profile_voluntary_switches(f, *args, **kwargs):    prof = cProfile.Profile(        lambda: _current_process.num_ctx_switches().voluntary)    prof.runcall(f, *args, **kwargs)    result = pstats.Stats(prof)    result.sort_stats("time")    result.print_stats() 

现在,让我们再次分析适用于网络的代码:

 >>> profile_voluntary_switches( ...   lambda: urlopen("https://pythonspeed.com").read()) ncalls tottime percall filename:lineno(function)     3  7.000  2.333 _ssl._SSLSocket.read     1  2.000  2.000 _ssl._SSLSocket.do_handshake     1  2.000  2.000 _socket.getaddrinfo     1  1.000  1.000 _ssl._SSLContext.set_default_verify_path     1  1.000  1.000 _socket.socket.connect 

现在,我们可以看到有关发生的有意上下文切换数量的信息,而不是等待时间数据。

请注意,有时您会在意想不到的地方看到有意的上下文切换。 我相信,由于内存页面错误而正在加载页面文件中的数据时,会发生这种情况。

总结


使用此处描述的代码分析技术会给系统带来一定的额外负担,从而大大降低程序运行速度。 但是,在大多数情况下,由于我们不分析处理器资源的使用情况,因此这不会导致结果的严重失真。

总的来说,应该指出的是,与计划工作有关的任何可衡量指标都适合进行分析。 例如,以下内容:

  • 读取( psutil.Process().read_count )和写入( psutil.Process().write_count )的数量。
  • 在Linux上,读取和写入的字节总数(psutil。Process Process().read_chars )。
  • 内存分配指示器(执行此类分析将需要一些工作;可以使用jemalloc来完成)。

可以在psutil文档中找到有关此列表的前两项的详细信息。

亲爱的读者们! 您如何分析Python应用程序?

Source: https://habr.com/ru/post/zh-CN464133/


All Articles