Oracle Solaris Studio 12.2:性能分析器

第 6 章 了解性能分析器及其数据

性能分析器读取收集器收集的事件数据,并将其转换为性能度量。将会针对目标程序结构中的各种元素(如指令、源代码行、函数和装入对象)计算度量。除了包含时间戳、线程 id、LWP id 和 CPU id 的标头外,为收集的每个事件记录的数据还包含以下两部分:

由于编译器所进行的插入、转换和优化,将度量与程序结构关联的过程并不总是简单易懂。本章介绍该过程,并讨论对性能分析器显示内容的影响。

本章包含以下主题:

数据收集的工作原理

运行数据收集的输出是实验,该实验在文件系统中存储为带有各种内部文件和子目录的目录。

实验格式

所有实验都必须具有以下三个文件:

此外,实验还具有表示整个处理过程中的分析事件的二进制数据文件。每个数据文件都具有一系列事件,如下面解释性能度量所述。对于每种类型的数据,都将使用单独的文件,但每个文件都由目标中的所有 LWP 共享。

对于基于时钟的分析或硬件计数器溢出分析,将数据写入时钟周期或计数器溢出调用的信号处理程序中。对于同步跟踪、堆跟踪、MPI 跟踪或开放式 MP 跟踪,从 LD_PRELOAD 环境变量在用户调用的常规例程上插入的 libcollector 例程写入数据。每个这样的插入例程都部分填充数据记录,然后调用用户调用的常规例程,在该例程返回时填充数据记录的其余部分,并将记录写入数据文件。

所有数据文件都按块进行内存映射和写入。以这样的方式填充记录以便始终具有有效的记录结构,这样就可以在写入时读取实验。缓冲区管理策略设计用于最大限度地减少 LWP 之间的争用和序列化。

实验可以选择性地包含文件名为 notes 的 ASCII 文件。使用 collect 命令的 -C comment 参数时会自动创建此文件。创建实验后,可以手动创建或编辑该文件。该文件的内容会置于实验标题之前。

archives 目录

每个实验都具有一个 archives 目录,该目录包含描述 map.xml 文件中引用的每个装入对象的二进制文件。这些文件由 er_archive 实用程序(它在数据收集结束时运行)生成。如果进程异常终止,则可能无法调用 er_archive 实用程序,在这种情况下,归档文件由 er_print 实用程序或分析器在实验上首次调用时写入。

子孙进程

子孙进程将其实验写入创始进程的实验目录内的子目录。

对这些新实验的命名可指示其衍生情况,如下所示:

例如,如果创始进程的实验名称为 test.1.er,则其第三个派生创建的子进程的实验为 test.1.er/_f3.er。如果该子进程执行新映像,则对应的实验名称为 test.1.er/_f3_x1.er。后续实验由与父实验相同的文件组成,但是它们没有后续实验(所有后续实验都由创始实验中的子目录表示),而且它们没有归档子目录(所有归档都在创始实验中进行)。

动态函数

目标创建动态函数的实验在 map.xml 文件中具有描述这些函数的附加记录,还具有一个附加文件 dyntext,该文件包含动态函数的实际指令的副本。生成动态函数的带注释反汇编时需要该副本。

Java 实验

Java 实验在 map.xml 文件中具有附加记录,这两个记录用于 JVM 软件因其内部目的而创建的动态函数和目标 Java 方法的动态编译 (HotSpot) 版本。

此外,Java 实验具有一个 JAVA_CLASSES 文件,该文件包含有关调用的所有用户 Java 类的信息。

使用 JVMTI 代理记录 Java 跟踪数据,该代理是 libcollector.so 的一部分。该代理接收映射到记录的跟踪事件中的事件。该代理还接收类装入和 HotSpot 编译(用于写入 JAVA_CLASSES 文件)的事件以及 map.xml 文件中 Java 编译的方法记录。

记录实验

可以使用以下三种不同方法记录实验:

分析器 GUI 中的“性能收集”窗口中运行 collect 实验。

collect 实验

使用 collect 命令记录实验时,collect 实用程序会创建实验目录,并设置 LD_PRELOAD 环境变量,以确保将 libcollector.so 及其他 libcollector 模块预装入到目标的地址空间。然后,该 collect 实用程序设置环境变量,将实验名称和数据收集选项通知 libcollector.so,并执行自己顶部的目标。

libcollector.so 及其相关模块负责写入所有实验文件。

创建进程的 dbx 实验

在启用数据收集的情况下使用 dbx 启动进程时,dbx 还会创建实验目录,并确保预装入 libcollector.so。然后 dbx 在其第一个指令前的断点处停止进程,并调用 libcollector.so 中的初始化例程以启动数据收集。

Java 实验不能由 dbx 收集,因为 dbx 使用 Java 虚拟机调试接口 (Java Virtual Machine Debug Interface, JVMDI) 代理进行调试,而该代理无法与数据收集所需的 Java 虚拟机工具接口 (Java Virtual Machine Tools Interface, JVMTI) 代理共存。

正在运行的进程上的 dbx 实验

在正在运行的进程上使用 dbx 启动实验时,dbx 会创建实验目录,但不能使用 LD_PRELOAD 环境变量。dbx 对目标进行交互式函数调用以打开 libcollector.so,然后调用 libcollector.so 初始化例程,就像它创建进程时那样。与 collect 实验中一样,数据由 libcollector.so 及其模块写入。

由于进程启动时 libcollector.so 不在目标地址空间中,因此取决于插入用户可调用函数(同步跟踪、堆跟踪、MPI 跟踪)的任何数据收集可能都不起作用。通常,符号已经解析为底层函数,因此无法发生插入。此外,以下子孙进程也取决于插入,对于 dbx 在正在运行的进程上创建的实验无法正常工作。

如果在使用 dbx 启动进程之前或者在使 dbx 附加到正在运行的进程之前已显式预装入 libcollector.so,可以收集跟踪数据。

解释性能度量

每个事件的数据都包含高精度时间戳、线程 ID、LWP ID 和处理器 ID。其中的前三项可用于在性能分析器中按时间、线程、LWP 或 CPU 过滤度量。有关处理器 ID 的信息,请参见 getcpuid(2) 手册页。在 getcpuid 不可用的系统上,处理器 ID 为 -1(它映射为“未知”)。

除了通用数据外,每个事件还生成特定的原始数据,将在以下各节中对此进行描述。每节还将介绍从原始数据派生的度量的准确性,以及数据收集对度量的影响。

基于时钟的分析

基于时钟的分析的事件特定数据由分析间隔计数的数组组成。在 Solaris OS 上,提供了间隔计数器。在分析间隔结束时,相应的间隔计数器加 1,并安排另一个分析信号。仅当 Solaris LWP 线程进入 CPU 用户模式时,才记录和重置数组。重置数组包括将用户 CPU 状态的数组元素设置为 1,将所有其他状态的数组元素设置为 0。在重置数组之前,进入用户模式时会记录数组数据。因此,数组包含自上次进入用户模式以来进入的每个微态的计数累积(内核为每个 Solaris LWP 维护十个微态)。在 Linux OS 上,不存在微态;唯一的间隔计数器是“用户 CPU 时间”。

在记录数据的同时记录调用栈。如果在分析间隔结束时 Solaris LWP 未处于用户模式,则在 LWP 或线程再次进入用户模式之前调用栈无法更改。因此,调用栈总是会在每个分析间隔结束时准确记录程序计数器的位置。

Solaris OS 上每个微观状态所服务于的度量表 6–1 所示。

表 6–1 内核微态如何服务于度量

内核微态 

说明 

度量名称 

LMS_USER

在用户模式下运行 

用户 CPU 时间 

LMS_SYSTEM

在系统调用或缺页时运行 

系统 CPU 时间 

LMS_TRAP

在出现任何其他陷阱时运行 

系统 CPU 时间 

LMS_TFAULT

在用户文本缺页时休眠 

文本缺页时间 

LMS_DFAULT

在用户数据缺页时休眠 

数据缺页时间 

LMS_KFAULT

在内核缺页时休眠 

其他等待时间 

LMS_USER_LOCK

等待用户模式锁定时休眠 

用户锁定时间 

LMS_SLEEP

由于任何其他原因而休眠 

其他等待时间 

LMS_STOPPED

已停止(/proc、作业控制或 lwp_stop

其他等待时间 

LMS_WAIT_CPU

等待 CPU 

等待 CPU 时间 

计时度量的准确性

计时数据是基于统计收集的,因此易于出现任何统计抽样方法的所有误差。对于时间非常短的运行(仅记录少量分析数据包),调用栈可能不能表示程序中使用大多数资源的各部分。因此应以足够长的时间或足够多的次数运行程序,以累积感兴趣的函数或源代码行的数百个分析数据包。

除了统计抽样误差外,收集和归属数据的方式以及程序在系统中前进方式也会引起特定的误差。以下是计时度量可能出现不准确或失真的一些情况:

除刚刚介绍的不准确性外,计时度量还会因收集数据的过程而失真。记录分析数据包所用的时间从不出现在程序的度量中,因为记录是由分析信号启动的。(这是相关性的另一个实例。记录过程中所用的用户 CPU 时间在所记录的任何微态之间分配。结果是对用户 CPU 时间度量过少记帐,而对其他度量过多记帐。记录数据所用的时间量通常不到缺省分析间隔的 CPU 时间的百分之几。

计时度量的比较

如果将通过在基于时钟的实验中进行分析所获得的计时度量与通过其他方式获得的时间进行比较,则应该注意以下问题。

对于单线程应用程序,为进程记录的 Solaris LWP 或 Linux 线程时间总计通常精确到千分之几(与同一进程的 gethrtime(3C) 返回的值相比)。CPU 时间可能与由同一进程的 gethrvtime(3C) 返回的值相差几个百分点。如果负载过重,则差异可能更加明显。但是,CPU 时间差异并不表示系统失真,并且为不同函数、源代码行等报告的相对时间也不会显著失真。

对于 Solaris OS 上使用未绑定线程的多线程应用程序,gethrvtime() 所返回的值的差异可能没有意义,因为 gethrvtime() 返回 LWP 的值,而线程可能随 LWP 的不同而不同。

性能分析器中报告的 LWP 时间可能与 vmstat 报告的时间有很大差异,因为 vmstat 报告 CPU 的汇总时间。如果目标进程具有的 LWP 比它所运行的系统具有的 CPU 多,则性能分析器显示的等待时间比 vmstat 报告的长。

出现在性能分析器的“统计数据”标签和 er_print 统计显示中的微态计时基于进程文件系统 /proc 使用报告,因此微态中所用时间的记录具有很高的准确性。有关更多信息,请参见 proc (4) 手册页。可以将这些计时与 <Total> 函数(它将程序作为一个整体表示)的度量进行比较,以获取聚集计时度量的准确性指示。但是,“统计数据”标签中显示的值可能包含其他基值,而 <Total> 的计时度量值中不包含这些基值。这些基值来自暂停数据收集的时间段。

用户 CPU 时间和硬件计数器循环时间是不同的,因为在将 CPU 模式切换到系统模式时会关闭硬件计数器。有关更多信息,请参见陷阱

同步等待跟踪

收集器通过跟踪对线程库 libthread.so 中函数的调用或对实时扩展库 librt.so 的调用来收集同步延迟事件。事件特定的数据由请求和授权的高精度时间戳(跟踪的调用的开始和结束)以及同步对象(例如,请求的互斥锁)的地址组成。线程 ID 和 LWP ID 是记录数据时的 ID。等待时间是请求时间和授权时间之间的差值。仅记录其等待时间超过指定阈值的事件。同步等待跟踪数据在授权时记录在实验中。

在完成导致延迟的事件之前,在其上安排有等待线程的 LWP 无法执行任何其他工作。等待所用时间同时显示为同步等待时间和用户锁定时间。用户锁定时间可能比同步等待时间长,因为同步延迟阈值筛去了短期延迟。

数据收集的开销使等待时间失真。该开销与收集的事件数成比例。通过增加用于记录事件的阈值,可以最大限度地减少开销中所用的等待时间部分。

硬件计数器溢出分析

硬件计数器溢出分析数据包括计数器 ID 和溢出值。该值可能大于计数器的溢出设置值,因为处理器在事件的溢出和记录之间会执行某些指令。尤其对循环和指令计数器来说,该值可能会更大,这些计数器的递增频率比诸如浮点运算或高速缓存未命中次数的计数器更快。记录事件中的延迟还意味着,通过调用栈记录的程序计数器地址并不精确对应于溢出事件。有关更多信息,请参见硬件计数器溢出的归属。另请参见陷阱的讨论。陷阱和陷阱处理程序可以导致报告的用户 CPU 时间和循环计数器报告的时间有很大差别。

动态更改其操作时钟频率的计算机上所记录的实验在基于周期的时间计数转换时显示不准确性。

收集的数据量取决于溢出值。选择过小的值可能会产生以下结果。

堆跟踪

收集器通过插入内存分配和解除分配函数 mallocreallocmemalignfree 来记录对这些函数的调用的跟踪数据。如果程序分配内存时忽视这些函数,则不记录跟踪数据。不记录 Java 内存管理(它使用不同的机制)的跟踪数据。

跟踪的函数可能从许多库中的任一个库装入。在性能分析器中看到的数据可能取决于从其装入给定函数的库。

如果程序在很短的时间段内发出对被跟踪函数的大量调用,则执行程序所用的时间可能会大大延长。额外的时间将用于记录跟踪数据。

数据空间分析

数据空间分析是对用于内存引用的硬件计数器分析的扩展。硬件计数器分析可以归属到用户函数、源代码行和指令的度量,但不归属到正在引用的数据对象的度量。缺省情况下,该收集器仅捕获用户指令地址。启用数据空间分析时,该收集器还捕获数据地址。回溯是用于获取支持数据空间分析的性能信息的技术。启用回溯时,收集器回顾在硬件计数器事件发生之前执行的装入或存储指令,以查找可导致该事件的候选指令。

要允许进行数据空间分析,目标必须是使用 -xhwcprof 标志和 -xdebugformat=dwarf -g 标志为 SPARC 体系结构编译的 C 程序。此外,收集的数据必须是与内存相关的计数器的硬件计数器分析数据,且必须在计数器名称之前放置 + 号。性能分析器包括两个与数据空间分析相关的标签(即“数据对象”标签和“数据布局”标签),以及用于内存对象的各种标签。

也可以通过在分析间隔之前放置加号 ( + ),使用时钟分析进行数据空间分析。

运行不带任何参数的 collect 将列出硬件计数器,并指明这些计数器是否与装入、存储或装入存储相关。请参见硬件计数器溢出分析数据

MPI 跟踪

MPI 跟踪基于修改的 VampirTrace 数据收集器。有关更多信息,请参见 Technische Universität Dresden Web 站点上的 Vampirtrace 用户手册。

调用栈和程序执行

调用栈是一系列程序计数器 (program counter, PC) 地址,表示来自程序内的指令。第一个 PC 称为 叶 PC,它位于堆栈的底部,是要执行的下一条指令的地址。下一个 PC 是对包含叶 PC 的函数的调用的地址;下一个 PC 是对该函数的调用的地址,依此类推,直至到达堆栈的顶部。每个这样的地址称为返回地址。记录调用栈的过程涉及从程序栈获取返回地址,这称为展开堆栈。有关展开失败的信息,请参见不完全的堆栈展开

调用栈中的叶 PC 用于将独占度量从性能数据分配到该 PC 所在的函数。堆栈中的每个 PC(包括叶 PC)用于将非独占度量分配到它所在的函数。

大多数时候,已记录调用栈中的 PC 自然地对应于出现在程序源代码中的函数,而且性能分析器的已报告度量直接对应于这些函数。但是,有时程序的实际执行并不与程序执行方式的简单直观模型相对应,而且性能分析器的已报告度量可能会引起混淆。有关此类情况的更多信息,请参见将地址映射到程序结构

单线程执行和函数调用

程序执行的最简单案例是单线程程序调用它自己的装入对象内的函数。

将程序装入到内存中开始执行时,会为其建立上下文,包括要执行的初始地址、初始寄存器集和堆栈(用于存储临时数据和用于跟踪函数如何相互调用的内存区域)。初始地址始终位于函数 _start()(它内置于每个可执行程序中)的开头。

程序运行时,将按顺序执行指令,直至遇到分支指令,该指令以及其他指令可能表示函数调用或条件语句。在分支点上,控制权转移到分支目标指定的地址,然后从该地址继续执行。(通常,已提交分支后的下一条指令以供执行:此指令称为分支延迟槽指令。但是,有些分支指令会取消分支延迟槽指令的执行。

当执行表示调用的指令序列时,返回地址被放入寄存器,且在被调用函数的第一条指令处继续执行。

在大多数情况下,在被调用函数的前几个指令中的某个位置,一个新(用于存储有关函数的信息的内存区域)会被推到堆栈上,而返回地址被放入该帧。然后,在被调用函数本身调用其他函数时可以使用用于返回地址的寄存器。函数即将返回时,将其帧从堆栈中弹出,而且控制权返回到从其调用该函数的地址。

共享对象之间的函数调用

一个共享对象中的函数调用另一个共享对象中的函数时,其执行情况比在程序内对函数的简单调用更复杂。每个共享对象都包含一个程序链接表 (Program Linkage Table, PLT),该表包含位于该共享对象外部并从该共享对象引用的每个函数的条目。最初,PLT 中每个外部函数的地址实际上是 ld.so(即动态链接程序)内的地址。第一次调用这样的函数时,控制权将转移到动态链接程序,该动态链接程序会解析对实际外部函数的调用并为后续调用修补 PLT 地址。

如果在执行三个 PLT 指令之一的过程中发生分析事件,则 PLT PC 会被删除,并将独占时间归属到调用指令。如果在通过 PLT 条目首次调用期间发生分析事件,但是叶 PC 不是 PLT 指令之一,PLT 和 ld.so 中的代码引起的任何 PC 都将归属到人工函数 @plt 中,该函数将累计非独占时间。每个共享对象都存在一个这样的人工函数。如果程序使用 LD_AUDIT 接口,则可能从不修补 PLT 条目,而且来自 @plt 的非叶 PC 可能发生得更频繁。

信号

将信号发送到进程时,会发生各种寄存器和堆栈操作,使得发送信号时的叶 PC 看起来好像是对系统函数 sigacthandler() 的调用的返回地址。sigacthandler() 调用用户指定的信号处理程序,就像任何函数调用另一个函数一样。

性能分析器将信号传送产生的帧视为普通帧。传送信号时的用户代码显示为调用系统函数 sigacthandler(),而 sigacthandler() 又显示为调用用户的信号处理程序。来自 sigacthandler() 和任何用户信号处理程序以及它们调用的任何其他函数的非独占度量,都显示为中断函数的非独占度量。

收集器通过插入 sigaction(),以确保其处理程序在收集时钟数据时是 SIGPROF 信号的主处理程序,而在收集硬件计数器溢出数据时是 SIGEMT 信号的主处理程序。

陷阱

陷阱可以由指令或硬件发出,而且由陷阱处理程序捕获。系统陷阱是指通过指令启动的陷阱,它们会陷入内核。所有系统调用均使用陷阱指令实现。硬件陷阱的一些示例是浮点单元无法完成某个指令(例如,用于 UltraSPARC III 平台上的某些寄存器内容值的 fitos 指令)时发出的陷阱,或者指令没有在硬件中实现时发出的陷阱。

发出陷阱时,Solaris LWP 或 Linux 内核进入系统模式。在 Solaris OS 上,微态通常从用户 CPU 状态切换到陷阱状态,再切换到系统状态。处理陷阱所用的时间可以显示为系统 CPU 时间和用户 CPU 时间的组合,具体取决于切换微态的时间点。该时间被归属到用户代码中从其启动陷阱的指令(或归属到系统调用)。

对于某些系统调用,提供尽可能高效的调用处理被认为是很关键的。由这些调用生成的陷阱称为快速陷阱。生成快速陷阱的系统函数包括 gethrtimegethrvtime。在这些函数中,由于涉及到的开销,所以不会切换微态

在其他情况下,提供尽可能高效的陷阱处理也被认为是很关键的。其中的一些示例是 TLB(translation lookaside buffer,转换后备缓冲器)未命中以及寄存器窗口溢出和填充,其中不切换微态。

在这两种情况下,所用的时间都记录为用户 CPU 时间。但是,由于 CPU 模式已切换为系统模式,所以将关闭硬件计数器。因此,通过求出用户 CPU 时间和周期时间(最好在同一实验中记录)之间的差值,可以估算处理这些陷阱所用的时间。

有一种陷阱处理程序切换回用户模式的情况,那是 Fortran 中在 4 字节边界上对齐的 8 字节整数的未对齐内存引用陷阱。陷阱处理程序的帧出现在堆栈上,而对处理程序的调用可以出现在性能分析器中,归属到整数装入或存储指令。

指令陷入内核后,陷阱指令后的指令看起来要使用很长时间,这是因为它在内核完成陷阱指令的执行之前无法启动。

尾部调用优化

只要特定函数执行的最后一个操作是调用另一个函数,编译器就可以执行一种特定的优化。被调用方可重用来自调用方的帧,而不是生成新的帧,而且可从调用方复制被调用方的返回地址。此优化的动机是减小堆栈的大小,以及(在 SPARC 平台上)减少对寄存器窗口的使用。

假定程序源代码中的调用序列与如下所示类似:

A -> B -> C -> D

BC 进行尾部调用优化后,调用栈看起来好像是函数 A 直接调用函数 BCD

A -> B
A -> C
A -> D

也就是说,调用树被展平。使用 -g 选项编译代码时,尾部调用优化仅发生在编译器优化级别 4 或更高级别上。在不使用 -g 选项的情况下编译代码时,尾部调用优化发生在编译器优化级别 2 或更高级别上。

显式多线程

在 Solaris OS 中,简单程序在单个 LWP(lightweight process,轻量级进程)上的单个线程中执行。多线程可执行程序调用线程创建函数(执行的目标函数会传递到该函数)。目标退出时,会销毁线程。

Solaris OS 支持两种线程实现:Solaris 线程和 POSIX 线程 (Pthread)。从 Solaris 10 OS 开始,这两种线程实现都包括在 libc.so 中。

对于 Solaris 线程,新创建的线程从名为 _thread_start() 的函数开始执行,该函数调用在线程创建调用中传递的函数。对于涉及此线程执行的目标的任何调用堆栈,堆栈的顶部是 _thread_start(),与线程创建函数的调用方没有任何联系。因此,与所创建的线程关联的非独占度量最多仅向上传播至 _thread_start()<Total> 函数。除了创建线程外,Solaris 线程实现还在 Solaris 上创建 LWP 以执行线程。每个线程都绑定到特定的 LWP。

Pthread 在 Solaris 10 OS 中以及 Linux OS 中可用于显式多线程。

在这两种环境中,为创建新线程,应用程序会调用 Pthread API 函数 pthread_create(),将指针作为函数参数之一传递到应用程序定义的启动例程。

在 Solaris OS 上,新的 pthread 开始执行时,将会调用 _lwp_start() 函数。在 Solaris 10 OS 上,_lwp_start() 调用中间函数 _thr_setup(),该中间函数随后调用在 pthread_create() 中指定的应用程序定义的启动例程。

在 Linux OS 上,新的 pthread 开始执行时,将会运行 Linux 特定的系统函数 clone(),该系统函数调用另一个内部初始化函数 pthread_start_thread(),该初始化函数又调用在 pthread_create() 中指定的应用程序定义的启动例程。可用于收集器的 Linux 度量收集函数是线程特定的。因此,collect 实用程序运行时,会在 pthread_start_thread() 和应用程序定义的线程启动例程之间插入一个名为 collector_root() 的度量收集函数。

基于 Java 技术的软件执行概述

对于典型的开发者,基于 Java 技术的应用程序就像任何其他程序那样运行。此类应用程序从主入口点(通常名为 class.main,可以调用其他方法)开始,就像 C 或 C++ 应用程序那样。

对于操作系统,使用 Java 编程语言(纯 Java 或与 C/C++ 混合)编写的应用程序作为实例化 JVM 软件的进程运行。JVM 软件是从 C++ 源代码编译的,从 _start(它会调用 main 等)开始执行。它从 .class 和/或 .jar 文件读取字节码,并执行在该程序中指定的操作。可以指定的操作包括动态装入本机共享对象以及调用该对象内包含的各种函数或方法。

JVM 软件可以执行许多用传统语言编写的应用程序通常不能执行的操作。启动时,该软件会在其数据空间中创建许多动态生成的代码的区域。其中一个区域是用于处理应用程序的字节码方法的实际解释器代码。

在执行基于 Java 技术的应用程序期间,JVM 软件解释大多数方法;这些方法称为已解释的方法。Java HotSpot 虚拟机会在解释字节码以检测频繁执行的方法时监视性能。然后,Java HotSpot 虚拟机可能编译重复执行的方法,以生成这些方法的机器码。生成的方法称为已编译的方法。之后,虚拟机执行更高效的已编译方法,而不是解释方法的原始字节码。已编译的方法会被装入应用程序的数据空间,并且可能会在之后的某个时间点卸载它们。此外,还会在数据空间中生成其他代码以执行已解释代码和已编译代码之间的转换。

用 Java 编程语言编写的代码还可以直接调用本机编译的代码(C、C++ 或 Fortran);此类调用的目标称为本机方法。

用 Java 编程语言编写的应用程序本身就是多线程的,对于用户程序中的每个线程,都具有一个 JVM 软件线程。Java 应用程序还具有若干个内务处理线程,用于信号处理、内存管理和 Java HotSpot 虚拟机编译。

在 J2SE 中的 JVMTI 上,可通过各种方法实现数据收集。

Java 调用栈和机器调用栈

性能工具通过记录每个 Solaris LWP 或 Linux 线程生存期中的事件,以及发生事件时的调用栈来收集其数据。在执行任何应用程序的任意点上,调用栈表示程序在其执行中所处的位置以及它如何到达该位置。混合模型 Java 应用程序区别于传统 C、C++ 和 Fortran 应用程序的一个重要方面是,在运行目标的过程中的任何瞬间都存在两个有意义的调用栈:Java 调用栈和机器调用栈。这两个调用栈都在配置期间进行记录,并在分析期间进行协调。

基于时钟的分析和硬件计数器溢出分析

用于 Java 程序的基于时钟的分析和硬件计数器溢出分析的工作方式与用于 C、C++ 和 Fortran 程序的情况基本相同,不同之处在于前者会同时收集 Java 调用栈和机器调用栈。

Java 处理表示法

对于用 Java 编程语言编写的应用程序,有以下三种显示性能数据的表示法:Java 表示法、专家 Java 表示法和机器表示法。缺省情况下,将显示 Java 表示法(前提是数据支持它)。下一节汇总了这三种表示法的主要差异。

用户表示法

用户表示法按名称显示已编译的和已解释的 Java 方法,并以其自然形式显示本机方法。在执行过程中,可能存在已执行的特定 Java 方法的许多实例:已解释的版本,也许还有一个或多个已编译的版本。在 Java 表示法中,所有方法会被聚集显示为一个方法。缺省情况下,在分析器中选定此表示法。

Java 表示法中 Java 方法的 PC 与该方法中的方法 id 和字节码索引相对应;本机函数的 PC 与机器 PC 相对应。Java 线程的调用栈可能同时具有 Java PC 和机器 PC。它没有对应于 Java 内务处理代码(无 Java 表示法)的任何帧。在某些情况下,JVM 软件无法展开 Java 堆栈,将返回单个帧及特殊函数 <no Java callstack recorded>。通常,它占总时间的比例不会超过 5-10%。

Java 表示法中的函数列表针对所调用的 Java 方法和任何本机方法显示度量。调用方-被调用方面板显示 Java 表示法中的调用关系。

Java 方法的源代码对应于 .java 文件(从中编译源代码,每个源代码行上都有度量)中的源代码。任何 Java 方法的反汇编显示为其生成的字节码,以及针对每个字节码的度量和交错的 Java 源代码(如果可用)。

Java 表示法中的时间线仅显示 Java 线程。每个线程的调用栈与其 Java 方法一起显示。

当前不支持 Java 表示法中的数据空间分析。

专家用户表示法

专家 Java 表示法与 Java 表示法类似,不同之处是在专家 Java 表示法中公开了在 Java 表示法中抑制的一些 JVM 内部详细信息。对于专家 Java 表示法,时间线显示所有线程;内务处理线程的调用栈是本机调用栈。

机器表示法

机器表示法显示来自 JVM 软件本身而不是来自 JVM 软件解释的应用程序的函数。该表示法还显示所有已编译方法和本机方法。机器表示法看起来与用传统语言编写的应用程序的表示法相同。调用栈显示 JVM 帧、本地帧和编译方法帧。一些 JVM 帧表示已解释的 Java、已编译的 Java 和本机代码之间的转换代码。

针对 Java 源代码显示已编译方法的源代码;数据表示所选已编译方法的特定实例。已编译方法的反汇编显示生成的机器汇编程序代码,而不是 Java 字节码。调用方-被调用方关系显示所有开销帧,以及表示已解释方法、已编译方法和本机方法之间的转换的所有帧。

机器表示法中的时间线以条形图显示所有线程、LWP 或 CPU,而其中每项的调用栈都是机器表示法调用栈。

OpenMP 软件执行概述

OpenMP 应用程序的实际执行模型在 OpenMP 规范中进行了描述(例如,请参见《OpenMP Application Program Interface, Version 3.0》中的 1.3 节)。但是,该规范未描述对用户可能很重要的一些实现详细信息,Oracle 的实际实现是这样的:通过直接记录的分析信息,用户并不能轻松了解线程是如何交互的。

在任何单线程程序运行时,其调用栈会显示其当前位置,以及如何到达那里的跟踪,跟踪从名为 _start 的例程(该例程调用 main,后者又继续调用程序内的各种子例程)中的起始指令开始。如果子例程包含循环,则程序会重复执行循环内的代码,直至达到循环退出条件。然后继续执行下一个代码序列,依此类推。

通过 OpenMP(或通过自动并行化)并行化程序时,行为是不同的。该并行化程序的直观模型具有像单线程程序那样执行的主线程。当它到达并行循环或并行区域时,将出现其他从属线程(每个都是主线程的克隆),它们都并行执行循环或并行区域的内容,每个从属线程执行不同的工作块。在完成所有工作块时,所有线程都是同步的,从属线程将消失,而主线程继续运行。

并行化程序的实际行为并非如此简单。在 Oracle 实现中,当编译器为并行区域或循环(或任何其他 OpenMP 构造)生成代码时,将提取其内部的代码,并使之成为一个名为 mfunction 的独立函数。(也可以将它称为外联函数或循环体函数。)mfunction 的名称是对 OpenMP 构造类型、从中提取它的函数的名称以及该构造所在源代码行的行号进行编码的结果。这些函数的名称在分析器的专家模式和计算机模式下按以下形式显示,其中方括号中的名称是函数的实际符号表名称:

bardo_ -- OMP parallel region from line 9 [_$p1C9.bardo_]
atomsum_ -- MP doall from line 7 [_$d1A7.atomsum_]

还有其他形式的此类函数,它们是从其他源代码构造派生的,名称中的 OMP parallel region 会被替换为 MP constructMP doallOMP sections。在下面的讨论中,所有这些都统称为并行区域

执行并行循环内代码的每个线程都可以多次调用其 mfunction,每次调用执行循环内的一个工作块。在所有工作块完成时,每个线程调用库中的同步或归约例程;然后主线程继续,而从属线程变为空闲状态,等待主线程进入下一个并行区域。所有调度和同步都是通过对 OpenMP 运行时的调用处理的。

在其执行过程中,并行区域内的代码可能执行一个工作块,或者它可能与其他线程同步,或者选择要执行的其他工作块。它还可能调用其他函数,而这些函数又可能会再调用其他函数。在并行区域内执行的从属线程(或主线程)可能本身(或者通过它调用的函数)充当主线程,并进入它自己的并行区域,从而导致嵌套并行操作。

分析器基于调用堆栈的统计抽样收集数据,跨所有线程聚集其数据,并针对函数、调用方和被调用方、源代码行和指令,基于所收集数据的类型显示性能度量。分析器以以下三种模式之一提供有关 OpenMP 程序性能的信息:用户模式、专家模式和计算机模式。

有关 OpenMP 程序的数据收集的更多详细信息,请参见 OpenMP 用户社区 Web 站点上的《An OpenMP Runtime API for Profiling》

OpenMP 分析数据的用户模式视图

分析数据的用户模式显示尝试提供信息,好像程序按照OpenMP 软件执行概述中所述的直观模型实际执行一样。在计算机模式下显示的实际数据捕获运行时库 libmtsk.so(它不对应于模型)的实现详细信息。专家模式显示为匹配模型而改变的混合数据和实际数据。

在用户模式下,更改了分析数据的显示以便更好地匹配模型,在以下三个方面不同于记录的数据和机器模式显示:

人工函数

构造人工函数,并将其放置在用户模式和专家模式调用堆栈上,以反映线程在 OpenMP 运行时库中处于某个状态的事件。

定义了以下人工函数:

<OMP-overhead>

在 OpenMP 库中执行 

<OMP-idle>

从属线程,等待工作 

<OMP-reduction>

执行归约操作的线程 

<OMP-implicit_barrier>

在隐式屏障处等待的线程 

<OMP-explicit_barrier>

在显式屏障处等待的线程 

<OMP-lock_wait>

等待锁定的线程 

<OMP-critical_section_wait>

等待进入临界段的线程 

<OMP-ordered_section_wait>

等待轮流进入排序段的线程 

<OMP-atomic_section_wait>

等待 OpenMP 原子构造的线程。 

当线程处于对应于其中一个人工函数的 OpenMP 运行时状态时,会将该人工函数作为堆栈上的叶函数添加。当线程的实际叶函数处于 OpenMP 运行时中的任意位置时,<OMP-overhead> 将作为叶函数替换它。否则,从用户模式堆栈中忽略 OpenMP 运行时中的所有 PC。

对于 OpenMP 3.0 程序,不使用 <OMP-overhead> 人工函数。由“OpenMP 开销”度量替换人工函数。

用户模式调用栈

对于 OpenMP 实验,用户模式显示重构的调用堆栈,这些重构的调用堆栈类似于在不使用 OpenMP 的情况下编译程序时获取的调用堆栈。目的在于以与程序的直观了解相匹配的方式提供分析数据,而不是显示实际处理的所有详细信息。当 OpenMP 运行时库正在执行某些特定操作时,协调主线程和从属线程的调用堆栈,并将人工 <OMP-*> 函数添加到调用堆栈。

OpenMP 度量

处理 OpenMP 程序的时钟分析事件时,将显示两个度量(它们分别对应于 OpenMP 系统中的两种状态所用的时间):“OpenMP 工作”和“OpenMP 等待”。

只要从用户代码执行线程(不管串行执行还是并行执行),就会在“OpenMP 工作”中累计时间。只要线程正在等待某项,之后才能继续,就会在“OpenMP 等待”中累计时间,而不管等待是忙等待(自旋等待)还是休眠。这两个度量的总和与时钟分析中的“总 LWP 时间”度量相匹配。

在用户模式、专家模式和计算机模式下显示“OpenMP 等待”和“OpenMP 工作”度量。

OpenMP 分析数据的专家视图模式

查看专家视图模式下的 OpenMP 实验时,如果 OpenMP 运行时正在执行某些特定操作,您将看到格式为 <OMP-*> 的人工函数,这一点类似于用户视图模式。但是,专家视图模式单独显示表示并行化循环、任务等的编译器生成的 mfunction。用户模式会将这些编译器生成的 mfunction 与用户函数聚集在一起。

OpenMP 分析数据的计算机视图模式

机器模式显示所有线程和编译器生成的外联函数的本机调用栈。

在执行的各个阶段中,程序的实际调用栈与上面在直观模型中描述的有很大差异。计算机模式将调用堆栈显示为已度量,没有进行转换,且没有构造人工函数。但是,仍显示时钟分析度量。

在下面的每个调用栈中,libmtsk 表示 OpenMP 运行时库内调用栈中的一个或多个帧。就像屏障代码的内部实现或执行归约一样,出现哪些函数以及出现顺序的详细信息随 OpenMP 的发行版的不同而不同。

  1. 在第一个并行区域之前

    在进入第一个并行区域之前,只有一个线程,即主线程。调用堆栈与用户模式和专家模式下的完全相同。

    主线程 

    foo

    main

    _start

  2. 在并行区域中执行时

    主线程 

    从属线程 1 

    从属线程 2 

    从属线程 3 

    foo-OMP...

         

    libmtsk

         

    foo

    foo-OMP...

    foo-OMP...

    foo-OMP...

    main

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    在机器模式下,从属线程显示为在 _lwp_start 中启动,而不是在 _start(主线程在其中启动)中启动。(在线程库的某些版本中,该函数可能显示为 _thread_start。对 foo-OMP... 的调用表示为并行化区域生成的 mfunction。

  3. 所有线程都在屏障处时

    主线程 

    从属线程 1 

    从属线程 2 

    从属线程 3 

    libmtsk

         

    foo-OMP...

         

    foo

    libmtsk

    libmtsk

    libmtsk

    main

    foo-OMP...

    foo-OMP...

    foo-OMP...

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    与线程在并行区域中执行时不同,当线程在屏障处等待时,在 foo 和并行区域代码 foo-OMP... 之间没有来自 OpenMP 运行时的帧。原因是实际执行中不包括 OMP 并行区域函数,但 OpenMP 运行时处理寄存器,以便堆栈展开显示从最后执行的并行区域函数到运行时屏障代码的调用。如果没有它,在机器模式下将无法确定哪个并行区域与屏障调用相关。

  4. 离开并行区域之后

    主线程 

    从属线程 1 

    从属线程 2 

    从属线程 3 

    foo

         

    main

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    在从属线程中,没有用户帧位于调用栈上。

  5. 在嵌套并行区域中时

    主线程 

    从属线程 1 

    从属线程 2 

    从属线程 3 

    从属线程 4 

     

    bar-OMP...

         

    foo-OMP...

    libmtsk

         

    libmtsk

    bar

         

    foo

    foo-OMP...

    foo-OMP...

    foo-OMP...

    bar-OMP...

    main

    libmtsk

    libmtsk

    libmtsk

    libmtsk

    _start

    _lwp_start

    _lwp_start

    _lwp_start

    _lwp_start

不完全的堆栈展开

堆栈展开在调用栈和程序执行中定义。

堆栈展开可能由于多种原因而失败:

中间文件

如果使用 -E-P 编译器选项生成中间文件,则分析器将中间文件用于带注释的源代码,而不是原始源文件。使用 -E 生成的 #line 指令可能会导致为源代码行分配度量时出现问题。

如果函数中的指令没有行号(这些行号引用为生成该函数而编译的源文件),则带注释的源代码中会出现以下行:

function_name -- <instructions without line numbers>

在以下情况下可能缺少行号:

将地址映射到程序结构

将调用栈处理为 PC 值后,分析器会将这些 PC 映射到程序中的共享对象、函数、源代码行和反汇编行(指令)。本节将介绍这些映射。

进程映像

运行程序时,会从该程序的可执行文件对进程进行实例化。该进程在其地址空间中具有许多区域,其中一些区域是文本,表示可执行指令,而另一些区域是通常不执行的数据。在调用栈中记录的 PC 通常对应于程序的某个文本段中的地址。

进程中的第一个文本段从可执行文件本身派生。其他文本段对应于与可执行文件一起装入(在启动进程时,或由进程动态装入)的共享对象。调用栈中的 PC 基于记录调用栈时装入的可执行文件和共享对象进行解析。可执行文件和共享对象非常类似,它们统称为装入对象。

由于可以在程序执行过程中装入和卸载共享对象,因此在运行期间的不同时间,任何给定的 PC 可能对应于不同的函数。此外,当卸载共享对象,然后在不同地址上重新装入它时,不同时间的不同 PC 可能对应于同一函数。

装入对象和函数

每个装入对象,不管是可执行文件还是共享对象,都包含一个文本段(含有编译器生成的指令)、一个存储数据的数据段以及各种符号表。所有装入对象都必须包含 ELF 符号表,该符号表提供该对象中所有全局已知函数的名称和地址。使用 -g 选项编译的装入对象包含附加的符号信息,该信息可以扩充 ELF 符号表,并提供有关非全局函数的信息、有关函数来自的对象模块的附加信息以及使地址与源代码行相关联的行号信息。

术语函数用于描述一组表示源代码中所述的高级别操作的指令。该术语涵盖 Fortran 中所用的子例程,C++ 和 Java 编程语言中所用的方法等等。函数在源代码中进行了清晰的描述,通常其名称出现在表示一组地址的符号表中;如果程序计数器位于该组中,则程序正在该函数内执行。

原则上,装入对象文本段中的任何地址都可以映射到函数。调用栈上的叶 PC 和所有其他 PC 都使用完全相同的映射。大多数函数直接对应于程序的源模型。有些函数却不是这样;将在以下各节中介绍这些函数。

有别名的函数

通常,函数被定义为全局函数,这意味着其名称在程序中的所有位置都是已知的。全局函数的名称在可执行文件中必须唯一。如果在地址空间中存在多个具有同一给定名称的全局函数,则运行时链接程序将解析对其中之一的所有引用。从不执行其他全局函数,因此它们不会出现在函数列表中。在“摘要”标签中,可以看到包含所选函数的共享对象和对象模块。

在不同情况下,一个函数可以具有若干个不同的名称。此情况的一个非常常见的示例是,将所谓的弱符号和强符号用于同一代码段。强名称通常与对应的弱名称相同,不同之处是它具有一个前导下划线。线程库中的许多函数还具有 pthread 和 Solaris 线程的备用名称,以及强名称、弱名称和备用内部符号。在所有此类情况下,分析器的函数列表中仅使用一个名称。所选名称是给定地址处按字母顺序排序的最后一个符号。此选择通常对应于用户将使用的名称。在“摘要”标签中,将显示所选函数的所有别名。

非唯一函数名称

尽管有别名的函数反映同一代码段的多个名称,但是在某些情况下,多个代码段具有相同的名称:

来自剥离共享库的静态函数

静态函数通常在库中使用,因此库内部使用的名称不会与用户可能使用的名称发生冲突。库被剥离后,静态函数的名称将从符号表中删除。在这种情况下,分析器会为包含剥离静态函数的库中的每个文本区域生成人工名称。该名称的格式为 <static>@0x12345,其中 @ 符号后的字符串是库中文本区域的偏离量。分析器无法区分连续的剥离静态函数和单个这样的函数,因此可能出现两个或多个这样的函数,且其度量合并在一起。

剥离静态函数显示为从正确的调用方进行调用,例外情况为静态函数中的 PC 是出现在静态函数中保存指令后的叶 PC 时。如果没有符号信息,分析器不知道保存地址,无法断定是否将返回寄存器用作调用方。它会始终忽略返回寄存器。由于可以将若干个函数合并为一个 <static>@0x12345 函数,因此可能无法将实际调用方或被调用方与相邻函数区分开。

Fortran 备用入口点

Fortran 可使单个代码段具有多个入口点,允许调用方调用到函数的中间。在编译这样的代码时,它包含主入口点的序言 (prologue)、备用入口点的序言和函数的代码主体。每个序言为函数的最终返回设置堆栈,然后转移或下行到代码的主体。

每个入口点的序言代码始终对应于具有该入口点名称的文本区域,但是子例程主体的代码仅接收可能的入口点名称之一。接收的名称随编译器的不同而不同。

序言很少占用大量时间,而且对应于除了与子例程的主体关联的入口点之外的入口点的函数很少出现在分析器中。在具有备用入口点的 Fortran 子例程中表示时间的调用堆栈通常在子例程的主体而不是前言中具有 PC,而且只有与主体关联的名称才显示为被调用方。同样,来自子例程的所有调用都显示为从与子例程主体关联的名称进行。

克隆函数

编译器能够识别可以对其执行额外优化的函数调用。此类调用的一个示例是对其某些参数为常量的函数的调用。当编译器识别出它可以优化的特定调用时,会创建该函数的副本(称为克隆) 并生成优化代码。克隆函数名称是标识特定调用的重整名称 (mangled name)。分析器取消重整 (demangle) 该名称,并在函数列表中单独显示克隆函数的每个实例。每个克隆函数都具有不同的指令集,因此带注释的反汇编列表单独显示克隆函数。每个克隆函数都具有相同的源代码,因此带注释的源代码列表汇总了函数的所有副本的数据。

内联函数

内联函数是这样的函数:在函数的调用点上(而不是实际调用上)为其插入由编译器生成的指令。有两种类型的内联,执行它们都可提高性能,并且它们都影响分析器。

这两种类型的内联对于度量的显示具有相同的效果。出现在源代码中但已被内联的函数不出现在函数列表中,也不显示为它们内联到的函数的被调用方。原本在内联函数的调用点上显示为非独占度量的度量(表示被调用函数中所用的时间)实际上将显示为归属到调用点的独占度量(表示内联函数的指令)。


注 –

内联可能会使数据难以解释,因此在编译程序以进行性能分析时,可能希望禁用内联。


在某些情况下,甚至在函数被内联时,会留下所谓的外部函数 (out-of-line function)。某些调用点调用外部函数 (out-of-line function),而其他调用点使指令内联。在这样的情况下,函数出现在函数列表中,但归属到该函数的度量仅表示外部调用 (out-of-line call)。

编译器生成的主体函数

编译器并行化函数中的循环或具有并行化指令的区域时,将会创建初始源代码中不存在的新主体函数。OpenMP 软件执行概述中介绍了这些函数。

分析器将这些函数显示为常规函数,除了编译器生成的名称外,分析器还基于从其提取这些函数的函数为其分配名称。其独占度量和非独占度量表示在主体函数中所用的时间。此外,从其提取构造的函数显示每个主体函数的非独占度量。OpenMP 软件执行概述中介绍了实现这一点的方法。

内联一个包含并行循环的函数时,其编译器生成的主体函数的名称反映它所内联到的函数,而不是初始函数。


注 –

只有使用 -g 编译的模块才能取消重整 (demangle) 编译器生成主体函数的名称。


外联函数

可以在反馈优化编译期间创建外联函数。它们表示正常情况下不执行的代码,特别是在用于生成最终优化编译反馈的训练运行期间不执行的代码。一个典型的示例是,对来自库函数的返回值执行错误检查的代码;正常情况下从不运行错误处理代码。为改进分页和指令高速缓存行为,将这样的代码移动到地址空间的其他位置,并使其成为单独的函数。外联函数的名称对有关外联代码段的信息进行编码,包括从其提取代码的函数的名称和源代码中该段开头的行号。这些重整名称 (mangled name) 可能随发行版的不同而不同。分析器提供了函数名称的可读版本。

外联函数不会被真正调用,而是跳转到它们;同样,它们不返回,而是跳回。为了使该行为与用户的源代码模型更紧密地匹配,分析器将来自主函数的人工调用转嫁于其外联部分。

外联函数显示为常规函数,具有相应的非独占度量和独占度量。此外,外联函数的度量作为代码从中进行外联的函数的非独占度量添加。

有关反馈优化编译的更多详细信息,请参阅《C 用户指南》的附录 B、《C++ 用户指南》的附录 A 或《Fortran 用户指南》的第 3 章中对 -xprofile 编译器的选项的说明。

动态编译的函数

动态编译的函数是指程序执行时编译和链接的函数。收集器中并没有有关用 C 或 C++ 编写的动态编译函数的信息,除非用户使用收集器 API 函数提供所需的信息。有关 API 函数的信息,请参见动态函数和模块。如果未提供信息,则函数在性能分析工具中显示为 <Unknown>

对于 Java 程序,收集器获取有关由 Java HotSpot 虚拟机编译的方法的信息,无需使用 API 函数提供信息。对于其他方法,性能工具显示有关执行方法的 JVM 软件的信息。在 Java 表示法中,所有方法都与已解释版本合并在一起。在机器表示法中,单独显示每个 HotSpot 编译的版本,且为每个已解释的方法显示 JVM 函数。

<Unknown> 函数

在某些情况下,PC 不会映射到已知函数。在这样的情况下,PC 会映射到名为 <Unknown> 的特殊函数。

以下情况显示映射到 <Unknown> 的 PC:

<Unknown> 函数的调用方和被调用方表示调用堆栈中的上一个和下一个 PC,以常规方式处理。

OpenMP 特殊函数

构造人工函数,并将其放置到用户模式调用栈(这些用户模式调用栈反映线程在 OpenMP 运行时库内处于某个状态的事件)。定义了以下人工函数:

<OMP-overhead>

在 OpenMP 库中执行 

<OMP-idle>

从属线程,等待工作 

<OMP-reduction>

执行归约操作的线程 

<OMP-implicit_barrier>

在隐式屏障处等待的线程 

<OMP-explicit_barrier>

在显式屏障处等待的线程 

<OMP-lock_wait>

等待锁定的线程 

<OMP-critical_section_wait>

等待进入临界段的线程 

<OMP-ordered_section_wait>

等待轮流进入排序段的线程 

<JVM-System> 函数

在用户表示法中,<JVM-System> 函数表示 JVM 软件执行操作而不是运行 Java 程序所用的时间。在此时间间隔中,JVM 软件执行诸如垃圾收集和 HotSpot 编译之类的任务。缺省情况下,可在函数列表中看到 <JVM-System>

<no Java callstack recorded> 函数

<no Java callstack recorded> 函数类似于 <Unknown> 函数,但它仅用于 Java 表示法中的 Java 线程。当收集器从 Java 线程收到事件时,收集器会展开本机栈并调用到 JVM 软件中,以获取对应的 Java 栈。如果该调用由于任何原因而失败,则该事件会与人工函数 <no Java callstack recorded> 一起显示在分析器中。为了避免死锁,或展开 Java 栈时导致过度同步,JVM 软件可能会拒绝报告调用栈。

<Truncated-stack> 函数

分析器为记录调用栈中各个函数的度量所用的缓冲区大小是有限的。如果调用栈大小变得如此大而导致缓冲区变满,则对调用栈大小的任何进一步增加都将强制分析器删除函数分析信息。由于在大多数程序中,大部分独占 CPU 时间用在叶函数中,因此分析器删除堆栈底部不太重要的函数(从入口函数 _start()main() 开始)的度量。已删除函数的度量将合并到单个人工 <Truncated-stack> 函数中。<Truncated-stack> 函数也可能出现在 Java 程序中。

<Total> 函数

<Total> 函数是一个人工结构,用于将程序作为一个整体表示。除了归属到调用栈上的函数外,所有性能度量都归属到特殊函数 <Total>。该函数出现在函数列表的顶部,其数据可以用于为其他函数的数据提供透视。在“调用方-被调用方”列表中,它显示为所执行的任何程序的主线程中 _start() 的名义调用方,还显示为已创建线程的 _thread_start() 的名义调用方。如果堆栈展开是不完整的,<Total> 函数可能显示为 <Truncated-stack> 的调用方。

与硬件计数器溢出分析相关的函数

以下函数与硬件计数器溢出分析相关:

将性能数据映射到索引对象

索引对象表示可以通过每个包中记录的数据计算其索引的对象集。预定义的索引对象集包括:线程、CPU、样本和秒。其他索引对象可能是通过直接发出的或 .er.rc 文件中的 er_print indxobj_define 命令定义的。在分析器中,可以通过从“视图”菜单中选择“设置数据显示”,选择“标签”标签,然后单击“添加定制索引对象”按钮来定义索引对象。

对于每个包,将会计算索引,并将与包关联的度量添加到该索引处的索引对象。索引 -1 映射到 <Unknown> 索引对象。索引对象的所有度量都是独占度量,因为索引对象的分层表示都是没有意义的。

将数据地址映射到程序数据对象

当对应于内存操作的硬件计数器事件的 PC 已被处理,可以成功回溯到很可能引发事件的内存引用指令时,分析器将使用编译器在其硬件分析支持信息中提供的指令标识符和描述符派生关联的程序数据对象。

术语“数据对象”用于表示程序常量、变量、数组和聚集(如结构和联合),以及源代码中所述的各种聚集元素。数据对象的类型及其大小随源语言的不同而不同。许多数据对象是在源程序中显式命名的,而其他数据对象可能是未命名的。有些数据对象是从其他(更简单的)数据对象派生或聚集的,从而产生了一组丰富的、通常很复杂的数据对象。

每个数据对象都具有关联的范围,即在其中定义数据对象并可以引用数据对象的源程序区域,该区域可能是全局性的(如装入对象),也可能是特定的编译单元(目标文件)或函数。相同的数据对象可能是使用不同的范围定义的,或者是在不同的范围内以不同的方式引用的特定数据对象。

在启用回溯的情况下为内存操作的硬件计数器事件收集的数据派生度量被归属到关联的程序数据对象类型,并传播到包含数据对象和人工 <Total>(它被视为包含所有数据对象,其中包括 <Unknown><Scalars>)的任何聚集。<Unknown> 的不同子类型向上传播到 <Unknown> 聚集。下一节将介绍 <Total><Scalars><Unknown> 数据对象。

数据对象描述符

可以通过数据对象的声明类型和名称的组合来完整描述数据对象。简单的标量数据对象 {int i} 描述名为 i、类型为 int 的变量,而 {const+pointer+int p} 描述类型为 int、名为 p 的常量指针。类型名称中的空格将替换为下划线 (_),未命名的数据对象用短划线 (-) 名称表示,例如:{double_precision_complex -}

同样,对于 foo_t 类型的结构,将整个聚集表示为 {structure:foo_t}。聚集元素需要其容器的其他规范,例如,{structure:foo_t}.{int i} 表示 foo_t 类型的上一结构的 int 类型的成员 i。聚集本身也可以是(更大)聚集的元素,其对应描述符构造为聚集描述符的串联,并最终成为标量描述符。

虽然并不总是需要使用全限定描述符来消除数据对象的歧义,但是该描述符提供了完整的通用规范以协助标识数据对象。

<Total> 数据对象

<Total> 数据对象是一个人工结构,用于将程序的数据对象作为一个整体表示。除了归属到不同数据对象(以及它所属的任何聚集)外,所有性能度量都归属到特殊的数据对象 <Total>。该数据对象出现在数据对象列表的顶部,其数据可以用于为其他数据对象的数据提供透视。

<Scalars> 数据对象

聚集元素将其性能度量另外归属到其关联聚集的度量值,而所有标量常量和变量都将其性能度量另外归属到人工 <Scalars> 数据对象的度量值。

<Unknown> 数据对象及其元素

在许多情况下,不能将事件数据映射到特定的数据对象。在这样的情况下,将数据映射到名为 <Unknown> 的特殊数据对象及其元素之一,如下面所述。

将性能数据映射到内存对象

内存对象是内存子系统中的组件,如高速缓存行、页面和内存区。对象是通过从所记录的虚拟地址和/或物理地址计算的索引确定的。为虚拟页面和物理页面预定义了内存对象,其大小可以为 8 KB、64 KB、512 KB 和 4 MB。您可以在 er_print 实用程序中使用 mobj_define 命令定义其他内存对象。您也可以使用分析器中的“添加内存对象”对话框(通过单击“设置数据显示”对话框中的“添加定制内存对象”按钮,可以打开该对话框)定义定制内存对象。