Oracle Solaris Studio 12.2:性能分析器

将地址映射到程序结构

将调用栈处理为 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> 的调用方。

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

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