我有一个在Linux上运行的C++应用程序,我正在优化它.如何确定代码的哪些区域运行缓慢?
1> Mike Dunlave..:
如果您的目标是使用分析器,请使用其中一个建议器.
但是,如果你很匆忙,并且你可以手动中断调试器下的程序,而主观速度很慢,那么可以通过一种简单的方法来查找性能问题.
只需暂停几次,每次都看一下调用堆栈.如果有一些代码浪费了一定比例的时间,20%或50%或其他什么,那就是你在每个样本的行为中捕获它的概率.所以这大约是您将看到它的样本的百分比.没有必要的教育猜测.如果您确实猜到了问题所在,这将证明或反驳它.
您可能会遇到不同大小的多个性能问题.如果你清除其中任何一个,剩下的将占用更大的百分比,并且在随后的传球中更容易发现.当在多个问题上复合时,这种放大效应可以导致真正大规模的加速因子.
警告:程序员往往对这种技术持怀疑态度,除非他们自己使用它.他们会说分析器会给你这些信息,但只有当他们对整个调用堆栈进行采样时才会这样,然后让你检查一组随机的样本.(摘要是失去洞察力的地方.)调用图不会给你相同的信息,因为
他们没有在教学层面总结,并且
它们在递归的情况下给出令人困惑的摘要.
他们还会说它只适用于玩具程序,实际上它适用于任何程序,并且它似乎在更大的程序上更好地工作,因为它们往往有更多的问题要找.他们会说它有时会发现不是问题的东西,但只有在你看到一次之后才会这样.如果您在多个样本上看到问题,那就是真实的.
PS如果有一种方法可以在某个时间点收集线程池的调用堆栈样本,那么这也可以在多线程程序上完成,就像在Java中一样.
PPS作为一个粗略的概括,您在软件中拥有的抽象层越多,您就越有可能发现这是性能问题的原因(以及获得加速的机会).
补充:它可能不是很明显,但堆栈采样技术在递归的情况下同样有效.原因是通过删除指令节省的时间通过包含它的样本的分数来近似,而不管样本中可能出现的次数.
我经常听到的另一个反对意见是:" 它会在某个地方随机停止,它会错过真正的问题 ".这来自对现实问题的先验概念.性能问题的一个关键属性是他们无视期望.抽样告诉你一些问题,你的第一反应是难以置信.这很自然,但你可以确定它是否发现问题是真的,反之亦然.
补充:让我对其工作原理进行贝叶斯解释.假设有一些指令I
(调用或其他)在调用堆栈中占用了一小部分f
时间(因此成本太高).为简单起见,假设我们不知道是什么f
,但假设它是0.1,0.2,0.3,...... 0.9,1.0,并且每种可能性的先验概率为0.1,因此所有这些成本同样可能先验.
然后假设我们只采集2个堆栈样本,并且我们看到I
两个样本的指令,指定观察o=2/2
.这给了我们对频率f
的新估计I
,根据这个:
Prior
P(f=x) x P(o=2/2|f=x) P(o=2/2&&f=x) P(o=2/2&&f >= x) P(f >= x | o=2/2)
0.1 1 1 0.1 0.1 0.25974026
0.1 0.9 0.81 0.081 0.181 0.47012987
0.1 0.8 0.64 0.064 0.245 0.636363636
0.1 0.7 0.49 0.049 0.294 0.763636364
0.1 0.6 0.36 0.036 0.33 0.857142857
0.1 0.5 0.25 0.025 0.355 0.922077922
0.1 0.4 0.16 0.016 0.371 0.963636364
0.1 0.3 0.09 0.009 0.38 0.987012987
0.1 0.2 0.04 0.004 0.384 0.997402597
0.1 0.1 0.01 0.001 0.385 1
P(o=2/2) 0.385
最后一栏说,例如,f
> = 0.5 的概率为92%,高于之前假设的60%.
假设先前的假设是不同的.假设我们假设P(f = 0.1)是.991(几乎是确定的),并且所有其他可能性几乎是不可能的(0.001).换句话说,我们先前的确定性I
是便宜的.然后我们得到:
Prior
P(f=x) x P(o=2/2|f=x) P(o=2/2&& f=x) P(o=2/2&&f >= x) P(f >= x | o=2/2)
0.001 1 1 0.001 0.001 0.072727273
0.001 0.9 0.81 0.00081 0.00181 0.131636364
0.001 0.8 0.64 0.00064 0.00245 0.178181818
0.001 0.7 0.49 0.00049 0.00294 0.213818182
0.001 0.6 0.36 0.00036 0.0033 0.24
0.001 0.5 0.25 0.00025 0.00355 0.258181818
0.001 0.4 0.16 0.00016 0.00371 0.269818182
0.001 0.3 0.09 0.00009 0.0038 0.276363636
0.001 0.2 0.04 0.00004 0.00384 0.279272727
0.991 0.1 0.01 0.00991 0.01375 1
P(o=2/2) 0.01375
现在它说P(f> = 0.5)是26%,高于先前假设的0.6%.所以贝叶斯允许我们更新我们对可能成本的估计I
.如果数据量很小,它并不能准确地告诉我们成本是多少,只是它足够大,值得修复.
另一种看待它的方法叫做继承规则.如果你将硬币翻了2次,并且两次都出现了硬币,那么它对硬币的可能加权有什么影响呢?值得尊重的回答方式是说它是Beta分布,平均值(命中数+ 1)/(尝试次数+2)=(2 + 1)/(2 + 2)= 75%.
(关键是我们I
不止一次看到.如果我们只看到一次,除了f
> 0 之外,这并没有告诉我们多少.)
因此,即使是非常少量的样本也可以告诉我们很多关于它看到的指令成本的信息.(而且将看到他们的频率,平均成比例的成本.如果n
采取试样,f
是成本,那么I
将出现在nf+/-sqrt(nf(1-f))
样品.实施例,n=10
,f=0.3
,即3+/-1.4
样品).
添加,以直观地感受测量和随机堆栈采样之间的差异:
现在有一些分析器可以对堆栈进行采样,即使是在挂钟时间,但是出现的是测量(或热路径,或热点,从中得到)一个"瓶颈"很容易隐藏).他们没有告诉你(他们很容易)你自己的实际样本.如果您的目标是找到瓶颈,那么您需要查看的数量平均为 2除以所需的时间.因此,如果需要30%的时间,平均2/.3 = 6.7个样本将显示它,并且20个样本显示它的机会是99.2%.
以下是检查测量和检查堆叠样本之间差异的袖口图示.瓶颈可能是这样的一个大块,或许多小块,它没有任何区别.
测量是水平的; 它告诉你特定例程所花费的时间.采样是垂直的.如果有任何方法可以避免整个程序在那一刻所做的事情,如果你在第二个样本上看到它,你就找到了瓶颈.这就是产生差异的原因 - 看到花费时间的全部原因,而不仅仅是花费多少.
这基本上是一个穷人的采样分析器,这很好,但你冒着样本量太小的风险,这可能会给你完全虚假的结果.
@Crash:我不会讨论"穷人"部分:-)统计测量精度确实需要很多样本,但有两个相互冲突的目标 - 测量和问题定位.我专注于后者,你需要精确的位置,而不是精确的测量.例如,中间堆栈可以有单个函数调用A(); 占50%的时间,但它可以在另一个大型函数B,以及许多其他不昂贵的A()调用.功能时间的精确摘要可以是一个线索,但每个其他堆栈样本将查明问题.
......世界似乎认为用呼叫计数和/或平均时间注释的呼叫图就足够了.它不是.而令人遗憾的是,对于那些对调用堆栈进行抽样的人来说,最有用的信息就在他们面前,但为了"统计"的利益,他们会抛弃它.
我不是故意不同意你的技术.很明显,我非常依赖堆栈行走采样分析器.我只是指出现在有一些工具以自动方式完成,这一点非常重要,当你将功能从25%提高到15%并需要将其从1.2%降低到0.6%.
-1:干净的想法,但是如果你在即使是适度的绩效环境中获得报酬,这也浪费了每个人的时间.使用真实的分析器,这样我们就不必在你身后出现并解决实际问题.
此外,我倾向于考虑的性能问题不超过程序单个成本的3-4%.我们只是没有比那更大的热点; 相反,我们必须打倒一堆小问题,这样他们才能集体节省大笔开支.
@ 280Z28:程序员(有时包括我自己)倾向于诋毁不寻常的观点.
@ 280Z28:也许这不是"真正的"分析(只有43倍的加速):http://stackoverflow.com/questions/926266/performance-optimization-strategies-of-last-resort/927773#927773.缩放是在正确的轨道上.其他"真正的"剖析器容易出现常见的误解:http://stackoverflow.com/questions/1777556/alternatives-to-gprof/1779343#1779343
我怎么能感谢你这个精彩的回复?几个星期以来,我一直在墙上试着用复杂的设置找到问题(mpi->自定义运行时 - >线程等).我刚刚编写了一个简单的脚本,在其中一个节点上重复调用gdb(在我的情况下无关紧要),显示线程,并随机转储其中一个线程的跟踪.反复调用这个脚本我终于能够看到问题所在!我得到了一定程度的改进,事实证明问题是根本的,但也非常容易修复.我爱你Mike Dunlavey!:)
(请参阅我之前的评论.)对于那些不熟悉gdb的人,你所要做的就是"gdb -batch -x commands ./executable pid",其中命令包含类似(在我的情况下)"info threads"的内容.第一行,"bt"在第二行用于后退跟踪,"q"在最后一行用于退出,甚至可能不需要.我只是运行一个大工作,登录分布式计算机上的一个节点,然后运行该简单脚本.我注意到这个问题是由我的自定义运行时所做的假设造成的,这些假设与我的应用程序代码不兼容.休息是一线修复和胜利!:)
大多数现代采样分析器都会为每个样本提供调用堆栈.我特别想到VTune和xbperfview:他们在每个样本中走栈,并准确地向您显示哪些调用者对函数的成本有贡献.当然,没有此功能的采样器的用处要小得多.
谢谢.实际上它对递归没有问题.如果调用指令在样本上出现> 1次,那么仍然只有1个样本.指令花费的时间〜=它所在的样本数.
我不确定你得到了什么.如果我想要一个采样分析器显示每个功能的包容性和独占成本,并带有带注释的成本图,我使用xbperfview.如果我想要指令级信息,那么通过CPU的每个操作码都被记录下来,我可以看到哪些确切的PC地址受到了很大影响,我使用的是PIX,但是那么多的数据我只能收集一两帧的价值.
我仍然不清楚强调特定呼叫指令的意思.我的采样分析器告诉我,我的程序花费了7%的时间在函数F()中,其中60%来自函数A()中的调用,40%来自函数B()中的调用.在F()下面是G()和H(),当从F()调用时,它们总共加起来为6%,否则加1%.(请参阅dl.getdropbox.com/u/23059/jpix/xbperf.png)我可以使用跟踪工具来获取指令级数据,但通常只是为了查看哪些函数被频繁调用而哪些必须是收紧.
......这正是我想要的工具告诉我的.我希望它列出呼叫指令的地址,并告诉我每一个,它在堆栈中的总_inclusive_时间,在感兴趣的间隔中,作为该间隔的百分比.(不是执行计数,不是平均呼叫持续时间.)列表应按该百分比的降序排序,并且只需要显示前100个左右.我将指令的成本定义为该百分比.我知道目前没有工具可以提供这些信息(除了我几年前建立的信息).
@Mike Dunlavey:这是我在downvoting中的思考过程的引用.我的经验来自并行计算和现在的游戏行业.此外,如果答案被重新评选为详细的真实(科学?)分析建议清单,我不会低估你.这是一个彻底的答案,并且本身绝对正确; 它只是因为没有帮助某人在Linux上寻找C++的高质量分析器(也许是那些已经习惯了微软极其高质量的Windows用户).
@ 280Z28:够公平的.如果有人正在为Linux寻找高质量的分析器,我认为Zoom就在那里(差不多).我在Windows上没见过类似的,但这不是我的主要工作.MS产品往往具有非常好的质量,但如果他们对提供给您的最有效信息感到困惑,这无济于事.他们仍然会做一些事情,比如只看CPU时间以及我一直试图指出的所有其他东西.无论如何,谢谢你的交流.
这正是[Google cpu-profiler](http://google-perftools.googlecode.com/svn/trunk/doc/cpuprofile.html)的工作原理 - 它会对整个调用堆栈进行采样.
@klimkin:如果你知道告诉它什么,这是正确的.即使他们说_PROF更好,也更喜欢ITIMER_REAL.如果您知道要问,它似乎会给出行级别%.文本输出有6列 - 忽略1到4.图形输出 - 在真实软件中是一个老鼠的巢和充满节点无关,因为它们不是你的,并且随着递归而变得臃肿.&100 hz样品可能有点矫枉过正.
@ 280Z28:你现在可能已经忘记了这一点,但在*[sourceforge](http://sourceforge.net/projects/randompausedemo/)*上有一个新的代码示例(在C++中).它有一系列代表加速迭代的代码库,以及一个样本记录,以及一个可以在其中进行操作的短功率点.
@Waylon:谢谢.它很快.我希望它不那么新颖.它也不脏.我认为,为了定位问题,它比典型的分析器要好得多,因为它可以查明问题而不仅仅是提供线索.
......他们提供的所有示例都是带有浅层调用堆栈的小程序,其中性能问题确实是"热点",我将其定义为在大部分时间内找到PC的代码,因此必然不包含调用指令.对于调用堆栈上的每个INSTRUCTION,分析器应该做的是,ESPECIALLY调用指令,即包含该指令的样本的分数.在大型软件中,迄今为止最大的性能浪费是可以避免的函数调用,但没有一个分析器"得到它".为什么不???
...我忘了提一下,一般我想在挂钟时间上采样,而不是CPU时间.如果我做得太多,I/OI需要知道它.
@Norman:我读过Mike Spivey的摘要.他在调用图的上下文中明智地处理递归是件好事.问题是他仍然处于原始gprof的思维模式中,它重视1)功能,而不是指令(丢失状态信息),2)定时准确性,而不是问题定位(需要效率),3)调用图作为摘要(丢失信息),而不是检查代表性的程序状态(即堆栈样本).目标是准确地发现问题,而不是准确地测量问题.
@Mike Dunlavey:由于我的经验,"我投了反对"这个答案中描述的技术对结果的努力平衡要差得多.此外,这篇关于并发性分析的文章很长,但值得一读:http://msdn.microsoft.com/en-us/magazine/ee336027.aspx
@ 280Z28:是的,当涉及进程间通信时,我不得不求助于其他方法.关于"这个答案中描述的技术对结果的努力平衡要差得多." 我不知道是谁说的,或者他们是否真的尝试过,或者只是事先有意见.就像我说的那样,给我一个例子.计算机科学仍然是一门科学,而不是意见的平衡.
@Stephen:好的,我印象深刻.我假设短语"花费的时间百分比"实际上意味着"堆叠上的样本百分比"(因为很多人对此有点模糊).仅靠堆栈非常好,但通常[状态背景](http://stackoverflow.com/questions/2473666/tips-for-optimizing-c-net-programs/2474118#2474118)在证明行动是没有必要的.
@BatchyX:我唯一一次看到这个问题出现在W3.1的VC中,如果你点击"暂停",它就不会停止,直到下一个窗口消息!我们不是在分裂头发.如果我的邻居看到我的狗在5天内在他的院子里放松了三天并问我这件事,我是否有理由说"这并不意味着什么 - 你只有5个样品,他们甚至没有随机时间"?在使方法无效之前,非随机性必须非常糟糕.
我把这种技术称为"随机分析".当你试图找出从哪里开始的好工具.
@MikeDunlavey:您可以查看http://dtrace.org/blogs/brendan/2011/12/16/flame-graphs/.看起来像一个非常简洁的方式可视化你的意思.现在,如果我能找到一些方法来使用它来可视化callgrind转储......
嗨再次@Thorbjørn:)希望你做得很好.如果你已经尝试过,要解决一个真正的问题,并且以积极的心态,我们可以讨论利弊.我对多线程应用程序的唯一体验是存在繁重的跨线程异步协议.在那种情况下,我的经验是,随机暂停只会让我分道扬.. 为了解决剩下的问题,我必须恢复到日志记录技术.
我可能有点慢,迈克,但我已经在几个答案上阅读了你对这种方法的解释,我仍然有点不确定如何将它应用到我的问题领域.我们有一个肥皂网服务,使用太长时间(1-2秒)来计算回报答案.我将如何找到重点关注的内容?根据我的阅读,我会这样做:1.使用Web服务启动我的IDE.2.创建一个针对我的开发框的请求循环3.定期暂停调试器,注意我们在4处暂停的行和源文件.编译一行统计数据命中5. ??? 怎么办?
@oligofren:带点盐,因为我没有网络应用程序的经验,只有少量线程,有时还有异步协议(网络应用程序).基本问题是为什么*这个时刻*被花费了?忘记统计信息 - 查看每个示例,就好像它是一个可以找到的bug.当你看到它做了两次*时,你可以找到一种方法来避免,修复它并获得加速.异步协议的问题是很难一次性停止所有内容,因此您可以看到为什么要花费这一时刻.然后我使用费力的记录方法.
@oligofren:在第3步.你写的只记下行和源文件.你应该记下迈克在他的回答中解释的整个堆栈轨迹.
2> Ajay..:
您可以使用Valgrind以下选项
valgrind --tool=callgrind ./(Your binary)
它将生成一个名为的文件callgrind.out.x
.然后,您可以使用kcachegrind
工具来读取此文件.它会给你一个图形分析的结果,比如哪条线的成本是多少.
valgrind很棒,但要注意它会使你的程序变慢
还可以查看[Gprof2Dot](http://code.google.com/p/jrfonseca/wiki/Gprof2Dot),了解可视化输出的另一种惊人方法.`./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg`
@neves是Valgrind在实时分析"gstreamer"和"opencv"应用程序的速度方面不是很有帮助.
@Sebastian:`gprof2dot`现在在这里:https://github.com/jrfonseca/gprof2dot
3> Nazgob..:
我假设你正在使用GCC.标准解决方案是使用gprof进行分析.
确保-pg
在分析之前添加到编译中:
cc -o myprog myprog.c utils.c -g -pg
我还没有尝试过,但我听说过google-perftools的好消息.绝对值得一试.
相关问题在这里.
其他一些流行语如果gprof
不适合你:Valgrind,Intel VTune,Sun DTrace.
比尔,在vaglrind套房,你可以找到callgrind和massif.两者对于分析应用程序非常有用
@ Bill-the-Lizard:关于**gprof**的一些评论:http://stackoverflow.com/questions/1777556/alternatives-to-gprof/1779343#1779343
gprof -pg只是callstack分析的近似值.它插入mcount调用来跟踪哪些函数正在调用哪些函数.它使用标准时间采样,呃,时间.然后它将在函数foo()中采样的时间分配给foo()的调用者,以支持调用的数量.因此它不区分不同成本的呼叫.
我同意gprof是目前的标准.但需要注意的是,Valgrind用于描述程序的内存泄漏和其他与内存相关的方面,而不是速度优化.
另见我的gprof警告,http://stackoverflow.com/a/6540100/823636
4> Will..:
较新的内核(例如最新的Ubuntu内核)带有新的'perf'工具(apt-get install linux-tools
)AKA perf_events.
这些都带有经典的采样分析器(手册页)以及令人敬畏的时间表!
重要的是,这些工具可以是系统分析而不仅仅是流程分析 - 它们可以显示线程,进程和内核之间的交互,让您了解进程之间的调度和I/O依赖性.
很棒的工具!无论如何我有一个典型的"蝴蝶"视图,从"main-> func1-> fun2"风格开始?我似乎无法弄清楚......`perf report`似乎给了我调用父母的函数名...(所以它是一种倒置的蝴蝶视图)
即使像4.13这样的新内核也有eBPF用于分析.请访问http://www.brendangregg.com/blog/2015-05-15/ebpf-one-small-step.html和http://www.brendangregg.com/ebpf.html
5> 小智..:
我会使用Valgrind和Callgrind作为我的分析工具套件的基础.重要的是要知道Valgrind基本上是一个虚拟机:
(维基百科)Valgrind本质上是一个使用即时(JIT)编译技术的虚拟机,包括动态重新编译.原始程序中的任何内容都不会直接在主机处理器上运行.相反,Valgrind首先将程序转换为一种称为中间表示(IR)的临时,简单形式,这是一种处理器中立的,基于SSA的形式.在转换之后,在Valgrind将IR转换回机器代码并让主处理器运行它之前,工具(见下文)可以自由地在IR上进行任何转换.
Callgrind是一个构建于此的探查器.主要好处是您不必花费数小时来运行您的应用程序以获得可靠的结果.即使一次运行也足以获得坚如磐石,可靠的结果,因为Callgrind是一个非探测分析器.
建立在Valgrind上的另一个工具是Massif.我用它来分析堆内存使用情况.它很棒.它的作用是为你提供内存使用的快照 - 详细信息什么是内存百分比,以及世界卫生组织把它放在那里.此类信息可在应用程序运行的不同时间点获得.
6> Tõnu Samuel..:
valgrind --tool=callgrind
没有一些选项,运行的答案并不完全.我们通常不想在Valgrind下描述10分钟的慢启动时间,并希望在执行某项任务时对我们的程序进行分析.
所以这就是我推荐的.首先运行程序:
valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp
现在,当它工作并且我们想要开始分析时,我们应该在另一个窗口中运行:
callgrind_control -i on
这会打开分析.要关闭它并停止整个任务,我们可能会使用:
callgrind_control -k
现在我们在当前目录中有一些名为callgrind.out.*的文件.要查看分析结果,请使用:
kcachegrind callgrind.out.*
我建议在下一个窗口中单击"Self"列标题,否则显示"main()"是最耗时的任务."自我"显示每个功能本身需要花费多少时间,而不是与家属一起.
现在因为某些原因,callgrind.out.*文件总是空的.执行callgrind_control -d对于强制将数据转储到磁盘很有用.
不能.我通常的情况类似于整个MySQL或PHP或类似的大事.通常甚至不知道我想先分开什么.
或者在我的情况下,我的程序实际上将一堆数据加载到LRU缓存中,我想不要对其进行分析.因此,我在启动时强制加载缓存的子集,并仅使用该数据来分析代码(让OS + CPU管理缓存中的内存使用).它可以工作,但加载缓存是缓慢的,并且我试图在不同的上下文中分析代码的CPU密集,因此callgrind会产生严重污染的结果.
还有`CALLGRIND_TOGGLE_COLLECT`以编程方式启用/禁用收集;参见/sf/ask/17360801/
7> Rob_before_e..:
这是对Nazgob的Gprof回答的回应.
我在过去的几天里一直在使用Gprof,并且已经发现了三个重要的限制,其中一个我还没有在其他任何地方看到过记录:
除非您使用变通方法,否则它在多线程代码上无法正常工作
调用图被函数指针搞糊涂了.示例:我有一个函数调用multithread()
,它使我能够在指定的数组上多线程化指定的函数(都作为参数传递).然而,Gprof将所有调用multithread()
视为等效于计算儿童时间的目的.由于我传递的某些功能multithread()
比其他功能要长得多,因此我的调用图大多没用.(对于那些想知道线程是否是问题的人:不,multithread()
可以选择,并且在这种情况下,只在调用线程上顺序运行所有内容).
它在这里说"......呼叫数字是通过计数而非采样得出的.它们是完全准确的......".然而我发现我的调用图给了我5345859132 + 784984078作为我最常调用的函数的调用统计数据,其中第一个数字应该是直接调用,第二个递归调用(它们都来自自身).由于这暗示我有一个错误,我将长(64位)计数器放入代码并再次执行相同的操作.我的计数:5345859132直接和78094395406自我递归调用.那里有很多数字,所以我会指出我测量的递归调用是780亿,而Gprof是784m:100不同.两次运行都是单线程和未经优化的代码,一个是编译的,另一个是编译-g
的-pg
.
这是在64位Debian Lenny下运行的GNU Gprof(GNU Binutils for Debian)2.18.0.20080103,如果这有助于任何人.
8> Ehsan..:
使用Valgrind,callgrind和kcachegrind:
valgrind --tool=callgrind ./(Your binary)
生成callgrind.out.x.使用kcachegrind读取它.
使用gprof(add -pg):
cc -o myprog myprog.c utils.c -g -pg
(多线程,函数指针不太好)
使用google-perftools:
使用时间采样,显示I/O和CPU瓶颈.
英特尔VTune是最好的(免费用于教育目的).
其他: AMD Codeanalyst(自AMD CodeXL取代),OProfile,'perf'工具(apt-get install linux-tools)
9> fwyzard..:
对于单线程程序,可以使用igprof,即Ignominous Profiler:https ://igprof.org/ 。
它是一个采样探查器,遵循... long ...的答案,由Mike Dunlavey回答,它将把结果包装在可浏览的调用堆栈树中,并在每个函数中花费的时间或内存进行注释,无论是累积的还是每个功能。