软件工程/测试/性能分析简介
在软件工程中,程序性能分析、软件性能分析或简称性能分析,是一种动态程序分析(与静态代码分析相对),通过收集程序执行过程中收集的信息来调查程序的行为。这种分析的通常目的是确定程序的哪些部分需要优化——以提高其整体速度,减少其内存需求,或者有时两者兼而有之。
- (代码)性能分析器是一种性能分析工具,最常见的是仅测量函数调用的频率和持续时间,但除了更全面的性能分析器外,还有其他特定类型的性能分析器(例如内存性能分析器),能够收集广泛的性能数据。
- 指令集模拟器也——必然地——是一个性能分析器,可以测量程序从调用到终止的全部行为。
性能分析器使用各种各样的技术来收集数据,包括硬件中断、代码插装、指令集模拟、操作系统钩子和性能计数器。性能分析器的使用在性能工程过程中被“调用”。
程序分析工具对于理解程序行为极其重要。计算机架构师需要这样的工具来评估程序在新架构上的执行情况。软件编写人员需要工具来分析他们的程序并识别关键代码部分。编译器编写人员经常使用此类工具来了解他们的指令调度或分支预测算法的执行情况……(ATOM,PLDI,'94)
性能分析器的输出可能是:
- 观察到的事件的统计摘要(概要文件)
- 摘要概要文件信息通常显示在事件发生位置的源代码语句上,因此测量数据的大小与程序的代码大小成线性关系。
/* ------------ source------------------------- count */ 0001 IF X = "A" 0055 0002 THEN DO 0003 ADD 1 to XCOUNT 0032 0004 ELSE 0005 IF X = "B" 0055
- 记录事件的流(跟踪)
- 对于顺序程序,摘要概要文件通常就足够了,但是并行程序中的性能问题(等待消息或同步问题)通常取决于事件的时间关系,因此需要完整的跟踪才能了解正在发生的事情。
- (完整)跟踪的大小与程序的指令路径长度成线性关系,这使得它有些不切实际。因此,可以从程序中的一个点启动跟踪并在另一个点终止跟踪以限制输出。
- 与虚拟机的持续交互(例如通过屏幕显示进行持续或周期性监控)
- 这提供了在执行过程中的任何所需点切换跟踪开启或关闭的机会,以及查看有关(仍在执行)程序的正在进行的指标。它还提供了在关键点暂停异步进程以更详细地检查与其他并行进程的交互的机会。
性能分析工具存在于 1970 年代初的 IBM/360 和 IBM/370 平台上,通常基于定时器中断,这些中断在设定的定时器间隔记录程序状态字 (PSW) 以检测执行代码中的“热点”。这是采样(见下文)的早期示例。在 1974 年初,指令集模拟器允许进行完整的跟踪和其他性能监控功能。
Unix 上的性能分析器驱动的程序分析可以追溯到至少 1979 年,当时 Unix 系统包含一个基本的工具“prof”,它列出了每个函数以及它使用了多少程序执行时间。1982 年,gprof 将该概念扩展到完整的调用图分析[1]
1994 年,Digital Equipment Corporation 的 Amitabh Srivastava 和 Alan Eustace 发表了一篇论文,描述了 ATOM。[2]ATOM 是一个将程序转换为其自身性能分析器的平台。也就是说,在编译时,它将代码插入到要分析的程序中。插入的代码输出分析数据。这种修改程序以分析自身的技术被称为“插装”。
2004 年,gprof 和 ATOM 论文都出现在有史以来 50 篇最有影响力的 PLDI 论文列表中。[3]
扁平性能分析器根据调用计算平均调用时间,并且不根据被调用方或上下文细分调用时间。
调用图性能分析器显示函数的调用时间和频率,以及基于被调用方的相关调用链。但是上下文没有保留。
此处列出的编程语言具有基于事件的性能分析器
- Java:JVMTI(JVM 工具接口)API(以前称为 JVMPI(JVM 性能分析接口))为性能分析器提供了挂钩,用于捕获诸如调用、类加载、卸载、线程进入离开等事件。
- .NET:可以将性能分析代理作为 COM 服务器附加到 CLR。与 Java 类似,运行时随后会提供各种回调到代理中,用于捕获诸如方法 JIT/进入/离开、对象创建等事件。特别强大之处在于性能分析代理可以以任意方式重写目标应用程序的字节码。
- Python:Python 性能分析包括 profile 模块、hotshot(基于调用图)以及使用“sys.setprofile”函数来捕获诸如 c_{call,return,exception}、python_{call,return,exception} 等事件。
- Ruby:Ruby 也使用类似于 Python 的接口进行性能分析。存在 profile.rb、模块中的扁平性能分析器和 ruby-prof C 扩展。
一些性能分析器通过采样来运行。采样性能分析器使用操作系统中断定期探测目标程序的程序计数器。采样概要文件通常在数值上不太准确和具体,但允许目标程序以接近全速运行。
得到的数据并非精确值,而是统计近似值。实际误差通常大于一个采样周期。事实上,如果某个值是采样周期的n倍,则其预期误差为n个采样周期的平方根。[4]
在实践中,采样分析器通常可以比其他方法更准确地描绘目标程序的执行情况,因为它们对目标程序的侵入性较小,因此不会产生太多副作用(例如对内存缓存或指令解码管道的副作用)。此外,由于它们对执行速度的影响较小,因此可以检测到否则会被隐藏的问题。它们也相对不容易过高地评估小型、频繁调用的例程或“紧凑”循环的成本。它们可以显示在用户模式下花费的时间与可中断内核模式(例如系统调用处理)下花费的时间的相对数量。
尽管如此,处理中断的内核代码会导致轻微的CPU周期损失,缓存使用被转移,并且无法区分不可中断内核代码中发生的各种任务(微秒级活动)。
专用硬件可以超越这一点:一些最近的MIPS处理器JTAG接口具有一个PCSAMPLE寄存器,它以一种真正无法检测到的方式对程序计数器进行采样。
一些最常用的统计分析器包括AMD CodeAnalyst、Apple Inc. Shark、gprof、Intel VTune和Parallel Amplifier(Intel Parallel Studio的一部分)。
一些分析器会插装目标程序,添加额外的指令以收集所需的信息。
插装程序可能会导致程序性能发生变化,可能导致结果不准确和海森堡错误。插装总会对程序执行产生一些影响,通常会使其变慢。但是,插装可以非常具体,并且可以小心控制以使其影响最小。对特定程序的影响取决于插装点的放置和用于捕获跟踪的机制。对跟踪捕获的硬件支持意味着,在某些目标上,插装可以只在一个机器指令上。插装的影响通常可以从结果中推断出来(即通过减法消除)。
gprof是一个使用插装和采样的分析器示例。插装用于收集调用者信息,实际计时值通过统计采样获得。
- 手动:由程序员执行,例如,添加指令以显式计算运行时间,简单地计数事件或调用测量API,例如应用程序响应测量标准。
- 自动源代码级:根据插装策略,由自动工具添加到源代码中的插装。
- 编译器辅助:例如,“gcc -pg …”用于gprof,“quantify g++ …”用于Quantify。
- 二进制翻译:工具将插装添加到已编译的二进制文件中。例如:ATOM。
- 运行时插装:在执行之前直接对代码进行插装。程序运行完全由工具监督和控制。例如:Pin、Valgrind。
- 运行时注入:比运行时插装更轻量级。在运行时修改代码以跳转到帮助函数。例如:DynInst。
- 解释器调试选项可以在解释器遇到每个目标语句时启用性能指标的收集。字节码、控制表或JIT解释器是三个示例,它们通常完全控制目标代码的执行,从而能够提供极其全面的数据收集机会。
- 管理程序:通过在管理程序下运行(通常)未修改的程序来收集数据。例如:SIMMON。
- 模拟器和管理程序:通过在指令集模拟器下运行未修改的程序,以交互方式和选择方式收集数据。例如:SIMON(批处理交互式测试/调试)和IBM OLIVER(CICS交互式测试/调试)。
- Dunlavey,“使用从调用栈采样派生的指令级成本进行性能调整”,ACM SIGPLAN通告42,8(2007年8月),第4-8页。
- Dunlavey,“性能调整:全力以赴!”,Dr. Dobb's Journal,第18卷,第12期,1993年11月,第18-26页。
- 文章“速度至上——消除性能瓶颈”介绍了如何使用IBM Rational Application Developer对Java应用程序进行执行时间分析。
- 使用VTune™性能分析器分析运行时生成和解释的代码