原始文章可以在这里阅读。
你好!作为Ruby Profiler的开胃酒,我想谈谈现有Ruby和Python Profiler的工作方式。它还将帮助回答许多人问我的问题:“如何编写分析器?”
在本文中,我们将重点介绍处理器分析器(而不是内存/堆分析器)。我将介绍一些用于编写探查器的基本方法,提供代码示例,并展示Ruby和Python中流行的探查器的许多示例,并向您展示它们是如何工作的。
可能是文章中有错误(在准备编写本文时,我部分浏览了14个库的代码以进行概要分析,并且到目前为止我还不熟悉它们),因此,如果找到它们,请告诉我。 ...
2种类型的探查器
处理器分析器主要有两种类型:采样分析器和跟踪分析器。
跟踪分析器记录程序中的每个函数调用,最终提供报告。采样分析器采用统计方法,它们每隔几毫秒写入一次堆栈,并基于此数据生成报告。
使用采样探查器而不是跟踪探查器的主要原因是因为它是轻量级的。您每秒可以拍摄20或200张照片-不需要很多时间。如果您遇到严重的性能问题(花费80%的时间花费在调用一个缓慢的函数上),这些分析器将非常有效,因为每秒200张快照足以识别问题函数!
分析器
接下来,我将提供本文中讨论的探查器的一般摘要(从此处开始)。稍后我将解释本文中使用的术语(setitimer,rb_add_event_hook,ptrace)。有趣的是,所有分析器都是使用一小组基本功能实现的。
Python分析器
“ Gdb hacks”实际上不是Python探查器,它链接到一个网站,该网站解释了如何将hacker探查器实现为gdb的外壳程序脚本包装。这是专门针对Python的,因为更高版本的gbd实际上会为您部署Python堆栈。穷人喜欢发烧。
Ruby分析器
几乎所有这些探查器都存在于您的进程中,
在深入了解这些探查器之前,有一件非常重要的事情-除pyflame之外,所有这些探查器都在Python / Ruby进程中运行。如果您位于Python / Ruby程序中,通常可以轻松访问堆栈。例如,下面是一个简单的Python程序,该程序打印每个正在运行的线程的堆栈内容:
import sys
import traceback
def bar():
foo()
def foo():
for _, frame in sys._current_frames().items():
for line in traceback.extract_stack(frame):
print line
bar()
这是控制台输出。您可以看到它具有来自堆栈的函数名称,行号,文件名-如果进行概要分析,则可能需要它。
('test2.py', 12, '<module>', 'bar()')
('test2.py', 5, 'bar', 'foo()')
('test2.py', 9, 'foo', 'for line in traceback.extract_stack(frame):')
在Ruby中甚至更简单:您可以使用puts调用程序来获取堆栈。
这些探查器大多数是C的性能扩展,因此它们略有不同,但是针对Ruby / Python程序的此类扩展也可以轻松访问调用堆栈。
跟踪探查器如何工作
我在上表中列出了所有Ruby和Python跟踪配置文件:rblineprof,ruby-prof,line_profiler和cProfile。它们都以类似的方式工作。它们记录每个函数调用,并且是C扩展以减少开销。
它们如何工作?在Ruby和Python中,您都可以指定在发生各种解释器事件(例如“函数调用”或“代码执行行”)时触发的回调。调用回调时,它将写入堆栈以供以后分析。
准确查看这些回调在代码中的位置会很有帮助,因此我将链接到github上的相关代码行。
在Python中,您可以使用
PyEval_SetTrace
或自定义回调PyEval_SetProfile
。在文档部分中对此进行了描述在Python中进行性能分析和跟踪。它说:“PyEval_SetTrace
类似于PyEval_SetProfile
跟踪功能接收行号事件。”
代码:
line_profiler
使用PyEval_SetTrace
以下命令设置其回调:请参见line_profiler.pyx
第157行cProfile
使用PyEval_SetProfile
以下命令设置其回调:参见_lsprof.c
第693行(cProfile是使用lsprof实现的)
在Ruby中,您可以使用来自定义回调
rb_add_event_hook
。我找不到任何相关文档,但这就是它的外观
rb_add_event_hook(prof_event_hook,
RUBY_EVENT_CALL | RUBY_EVENT_RETURN |
RUBY_EVENT_C_CALL | RUBY_EVENT_C_RETURN |
RUBY_EVENT_LINE, self);
签名
prof_event_hook
:
static void
prof_event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
类似于
PyEval_SetTrace
Python,但形式更灵活-您可以选择要通知哪些事件(例如“仅函数调用”)。
代码:
tracing-
跟踪以这种方式实现的探查器的主要缺点是,它们为执行的每个函数/行调用添加了固定数量的代码。它可能使您做出错误的决定!例如,如果您对某事进行两种实现-一种实现具有很多函数调用,而另一种实现则花费相同的时间,则性能分析时,第一种实现具有很多函数调用的速度会变慢。
为了说明这一点,我创建了一个名为
test.py
以下内容的小文件,并比较了执行时间python -mcProfile test.py
和python test.py
。python. test.py
在大约0.6 s和python -mcProfile test.py
大约1 s内完成。因此,对于此特定示例,我cProfile
添加了额外的〜60%的开销。
该文档cProfile
说:
Python的解释性质增加了太多的运行时开销,以至于确定性分析倾向于在普通应用程序中增加一点处理开销。
这似乎是一个很合理的声明-前面的示例(进行了350万个函数调用,仅此而已)显然不是常规的Python程序,几乎任何其他程序的开销都较小。
我还没有检查过
ruby-prof
Ruby跟踪分析器,但是它的自述文件指出:
大多数程序的运行速度大约慢一倍,而高度递归的程序(例如Fibonacci系列测试)的运行速度慢三倍。
抽样探查器通常如何工作:setitimer
是时候谈论第二种分析器了:采样分析器!
Ruby和Python中的大多数采样分析器都是使用系统调用实现的
setitimer
。这是什么?
假设您要每秒对程序堆栈进行快照50次。可以按照以下步骤进行:
- 要求Linux内核每20毫秒发送一次信号(使用系统调用
setitimer
); - 当接收到信号时,为堆栈快照注册信号处理程序;
- 分析完成后,请Linux停止向您发送信号并提供结果!
如果您想看到
setitimer
实现采样分析器的实际用例,我认为stacksampler.py
最好的例子是一个有用的,可工作的分析器,在Python中大约有100行。这太酷了!在Python中只占用100行
的原因
stacksampler.py
是,当您将Python函数注册为信号处理程序时,该函数将传递到程序的当前堆栈中。因此,stacksampler.py
注册信号处理程序非常容易:
def _sample(self, signum, frame):
stack = []
while frame is not None:
stack.append(self._format_frame(frame))
frame = frame.f_back
stack = ';'.join(reversed(stack))
self._stack_counts[stack] += 1
它只是从帧中弹出堆栈,并增加了查看特定堆栈的次数。很简单!非常酷!
让我们看看他们使用的所有其他探查器,
setitimer
并找出它们在代码中的位置setitimer
:
stackprof
(Ruby):stackprof.c
118perftools.rb
(Ruby): , , , , gem (?)stacksampler
(Python):stacksampler.py
51statprof
(Python):statprof.py
239vmprof
(Python):vmprof_unix.c
294
重要的是
setitimer
-您需要决定如何计算时间。您要实时20ms吗? 20ms的用户cpu时间? 20ms用户+系统cpu时间?如果仔细查看上面的链接,您会注意到这些探查器实际上使用了不同的东西setitimer
-有时行为是可自定义的,有时不是可定制的。手册页很setitimer
短,非常值得阅读所有可能的配置。
@mgedmin
在Twitter上指出了一个有趣的用例setitimer
。此问题和此问题揭示了更多细节。
基于的探查器的一个有趣的缺点
setitimer
-什么计时器触发信号!信号有时会中断系统调用!系统调用有时需要几毫秒!如果拍摄快照太频繁,则可以使程序无限期地执行系统调用!
不使用setitimer的采样探查器
有几个不使用的采样分析器
setitimer
:
pyinstrument
使用PyEval_SetProfile
(因此有点像跟踪分析器),但是在调用跟踪回调时,它并不总是收集堆栈快照。这是选择堆栈跟踪快照时间的代码。在此博客中了解有关此解决方案的更多信息。(基本上:setitimer
仅允许您在Python中剖析主线程)pyflame
使用系统调用在进程之外描述Python代码ptrace
。他使用一个循环来拍照,然后睡眠一段时间,然后再次执行相同的操作。这是电话等待。python-flamegraph
采取了类似的方法,它在Python进程中启动了一个新线程,并重新获得了堆栈跟踪,休眠和循环。这是一个等待电话。
所有这三个事件探查器均拍摄实时快照。
Pyflame博客文章
除了之外,我几乎所有时间都花在了Profiler上
pyflame
,但实际上我最感兴趣的是,它从一个单独的进程中对Python程序进行了配置,这就是为什么我希望我的Ruby Profiler可以类似地工作。
当然,一切都比我描述的要复杂。我不会详细介绍,但是Evan Klitzke在他的博客上写了很多关于此的好文章:
- Pyflame:Uber Engineering的Ptracing Profiler for Python -pyflame简介
- Pyflame Dual Interpreter Mode-它如何同时支持Python 2和Python 3
- 意外的Python ABI更改-关于添加对Python 3.6的支持;
- 转储多线程Python堆栈;
- Pyflame包装;
ptrace
和Python中的系统调用;- 使用ptrace获得乐趣和利润,ptrace(续);
可以在eklitzke.org上找到更多信息。这些都是非常有趣的事情,我将更详细地进行阅读-也许它
ptrace
会比process_vm_readv
实现Ruby分析器更好!它的process_vm_readv
开销较小,因为它不会停止该过程,但由于它不会停止该过程,因此也会给您带来不正确的快照:)。在我的实验中,获取有冲突的图片不是什么大问题,但是我认为在这里我将进行一系列实验。
今天就这些!
在这篇文章中,我没有涉及很多重要的细微之处-例如,我基本上说过-
vmprof
和stacksampler
-相似(它们不是-vmprof
支持字符串分析和用C编写的Python函数的分析,我认为这会使探查器更加复杂。但是它们具有一些相同的基本原理,因此我认为今天的审查将是一个很好的起点。
有和没有pytest的TDD。免费网络研讨会