跳过导航链接 | |
退出打印视图 | |
![]() |
Oracle Solaris Studio 12.3:性能分析器 Oracle Solaris Studio 12.3 Information Library (简体中文) |
将调用堆栈处理为 PC 值后,分析器会将这些 PC 映射到程序中的共享对象、函数、源代码行和反汇编行(指令)。本节将介绍这些映射。
运行程序时,会从该程序的可执行文件对进程进行实例化。该进程在其地址空间中具有许多区域,其中一些区域是文本,表示可执行指令,而另一些区域是通常不执行的数据。在调用堆栈中记录的 PC 通常对应于程序的某个文本段中的地址。
进程中的第一个文本段从可执行文件本身派生。其他文本段对应于与可执行文件一起装入(在启动进程时,或由进程动态装入)的共享对象。调用堆栈中的 PC 基于记录调用堆栈时装入的可执行文件和共享对象进行解析。可执行文件和共享对象非常类似,它们统称为装入对象。
由于可以在程序执行过程中装入和卸载共享对象,因此在运行期间的不同时间,任何给定的 PC 可能对应于不同的函数。此外,当卸载共享对象,然后在不同地址上重新装入它时,不同时间的不同 PC 可能对应于同一函数。
每个装入对象,不管是可执行文件还是共享对象,都包含一个文本段(含有编译器生成的指令)、一个存储数据的数据段以及各种符号表。所有装入对象都必须包含 ELF 符号表,该符号表提供该对象中所有全局已知函数的名称和地址。使用 -g 选项编译的装入对象包含附加的符号信息,该信息可以扩充 ELF 符号表,并提供有关非全局函数的信息、有关函数来自的对象模块的附加信息以及使地址与源代码行相关联的行号信息。
术语函数用于描述一组表示源代码中所述的高级别操作的指令。该术语涵盖 Fortran 中所用的子例程,C++ 和 Java 编程语言中所用的方法等等。函数在源代码中进行了清晰的描述,通常其名称出现在表示一组地址的符号表中;如果程序计数器位于该组中,则程序正在该函数内执行。
原则上,装入对象文本段中的任何地址都可以映射到函数。调用堆栈上的叶 PC 和所有其他 PC 都使用完全相同的映射。大多数函数直接对应于程序的源模型。有些函数却不是这样;将在以下各节中介绍这些函数。
通常,函数被定义为全局函数,这意味着其名称在程序中的所有位置都是已知的。全局函数的名称在可执行文件中必须唯一。如果在地址空间中存在多个具有同一给定名称的全局函数,则运行时链接程序将解析对其中之一的所有引用。从不执行其他全局函数,因此它们不会出现在函数列表中。在“摘要”标签中,可以看到包含所选函数的共享对象和对象模块。
在不同情况下,一个函数可以具有若干个不同的名称。此情况的一个非常常见的示例是,将所谓的弱符号和强符号用于同一代码段。强名称通常与对应的弱名称相同,不同之处是它具有一个前导下划线。线程库中的许多函数还具有 pthread 和 Solaris 线程的备用名称,以及强名称、弱名称和备用内部符号。在所有此类情况下,分析器的函数列表中仅使用一个名称。所选名称是给定地址处按字母顺序排序的最后一个符号。此选择通常对应于用户将使用的名称。在“摘要”标签中,将显示所选函数的所有别名。
尽管有别名的函数反映同一代码段的多个名称,但是在某些情况下,多个代码段具有相同的名称:
有时,由于模块化原因,函数被定义为静态的,这意味着其名称仅在程序的某些部分(通常为单个已编译的目标模块)中是已知的。在这样的情况下,引用程序完全不同部分的若干个同名函数将出现在分析器中。在“摘要”标签中,提供了其中每个函数的目标模块名称以便将它们区分开。此外,选择这些函数中的任何一个都可以用于显示该特定函数的源代码、反汇编以及调用方和被调用方。
有时,程序使用库中具有函数弱名称的包装函数或插入函数,并取代对该库函数的调用。有些包装函数调用库中的原始函数,在这种情况下,名称的两个实例都出现在分析器函数列表中。这样的函数来自不同的共享对象和不同的目标模块,这样它们彼此可以区分开。收集器包装某些库函数,而且包装函数和实际函数都可以出现在分析器中。
静态函数通常在库中使用,因此库内部使用的名称不会与用户可能使用的名称发生冲突。库被剥离后,静态函数的名称将从符号表中删除。在这种情况下,分析器会为包含剥离静态函数的库中的每个文本区域生成人工名称。该名称的格式为 <static>@0x12345,其中 @ 符号后的字符串是库中文本区域的偏离量。分析器无法区分连续的剥离静态函数和单个这样的函数,因此可能出现两个或多个这样的函数,且其度量合并在一起。
剥离静态函数显示为从正确的调用方进行调用,例外情况为静态函数中的 PC 是出现在静态函数中保存指令后的叶 PC 时。如果没有符号信息,则分析器不知道保存地址,无法断定是否将返回寄存器用作调用方。它会始终忽略返回寄存器。由于可以将若干个函数合并为一个 <static>@0x12345 函数,因此可能不能将实际调用方或被调用方与相邻函数区分开。
Fortran 可使单个代码段具有多个入口点,允许调用方调用到函数的中间。在编译这样的代码时,它包含主入口点的序言 (prologue)、备用入口点的序言和函数的代码主体。每个序言为函数的最终返回设置堆栈,然后转移或下行到代码的主体。
每个入口点的序言代码始终对应于具有该入口点名称的文本区域,但是子例程主体的代码仅接收可能的入口点名称之一。接收的名称随编译器的不同而不同。
序言很少占用大量时间,而且对应于除了与子例程的主体关联的入口点之外的入口点的函数很少出现在分析器中。在具有备用入口点的 Fortran 子例程中表示时间的调用堆栈通常在子例程的主体而不是前言中具有 PC,而且只有与主体关联的名称才显示为被调用方。同样,来自子例程的所有调用都显示为从与子例程主体关联的名称进行。
编译器能够识别可以对其执行额外优化的函数调用。此类调用的一个示例是对其某些参数为常量的函数的调用。当编译器识别出它可以优化的特定调用时,会创建该函数的副本(称为克隆) 并生成优化代码。克隆函数名称是标识特定调用的改编名称 (mangled name)。分析器取消改编 (demangle) 该名称,并在函数列表中单独显示克隆函数的每个实例。每个克隆函数都具有不同的指令集,因此带注释的反汇编列表单独显示克隆函数。每个克隆函数都具有相同的源代码,因此带注释的源代码列表汇总了函数的所有副本的数据。
内联函数是这样的函数:在函数的调用点上(而不是实际调用上)为其插入由编译器生成的指令。有两种类型的内联,执行它们都可提高性能,并且它们都影响分析器。
C++ 内联函数定义。此情况下内联的理论基础是,调用函数的成本比内联函数完成的工作要大得多,因此最好只是在调用点上插入函数的代码,而不是设置调用。通常,访问函数被定义为进行内联,因为它们通常仅需要一条指令。使用 -g 选项进行编译时,会禁用函数内联;使用 -g0 编译时,允许函数内联,这是建议的做法。
在高优化级别(4 和 5)上,编译器执行显式或自动内联。甚至在打开 -g 时,也会执行显式和自动内联。这种类型内联的理论基础可能在于,节省了函数调用的成本,但更为常见的目的是提供可以优化寄存器使用和指令调度的更多指令。
这两种类型的内联对于度量的显示具有相同的效果。出现在源代码中但已被内联的函数不出现在函数列表中,也不显示为它们内联到的函数的被调用方。否则在内联函数的调用点上显示为非独占度量的度量(表示被调用函数中所用的时间)实际上将显示为归属到调用点的独占度量(表示内联函数的指令)。
注 - 内联可能会使数据难以解释,因此在编译程序以进行性能分析时,可能希望禁用内联。
在某些情况下,甚至在函数被内联时,会留下所谓的外部函数 (out-of-line function)。某些调用点调用外部函数 (out-of-line function),而其他调用点使指令内联。在这样的情况下,函数出现在函数列表中,但归属到该函数的度量仅表示外部调用 (out-of-line call)。
编译器并行化函数中的循环或具有并行化指令的区域时,将会创建初始源代码中不存在的新主体函数。OpenMP 软件执行概述中介绍了这些函数。
在用户模式下,分析器不显示这些函数。在专家模式和计算机模式下,分析器将这些函数显示为常规函数,除了编译器生成的名称外,分析器还基于从其提取这些函数的函数为其分配名称。其独占度量和非独占度量表示在主体函数中所用的时间。此外,从其提取构造的函数显示每个主体函数的非独占度量。OpenMP 软件执行概述中介绍了实现这一点的方法。
内联一个包含并行循环的函数时,其编译器生成的主体函数的名称反映它所内联到的函数,而不是初始函数。
注 - 只有使用 -g 编译的模块才能取消改编 (demangle) 编译器生成主体函数的名称。
可以在反馈优化编译期间创建外联函数。它们表示正常情况下不执行的代码,特别是在用于生成最终优化编译反馈的训练运行期间不执行的代码。一个典型的示例是,对来自库函数的返回值执行错误检查的代码;正常情况下从不运行错误处理代码。为改进分页和指令高速缓存行为,将这样的代码移动到地址空间的其他位置,并使其成为单独的函数。外联函数的名称对有关外联代码段的信息进行编码,包括从其提取代码的函数的名称和源代码中该段开头的行号。这些改编名称 (mangled name) 可能随发行版的不同而不同。分析器提供了函数名称的可读版本。
外联函数不会被真正调用,而是跳转到它们;同样,它们不返回,而是跳回。为了使该行为与用户的源代码模型更紧密地匹配,分析器将来自主函数的人工调用转嫁于其外联部分。
外联函数显示为常规函数,具有相应的非独占度量和独占度量。此外,外联函数的度量作为代码从中进行外联的函数的非独占度量添加。
有关反馈优化编译的更多详细信息,请参见下列手册之一中的 -xprofile 编译器选项说明:
动态编译的函数是指程序执行时编译和链接的函数。收集器中并没有有关用 C 或 C++ 编写的动态编译函数的信息,除非用户使用收集器 API 函数提供所需的信息。有关 API 函数的信息,请参见动态函数和模块。如果未提供信息,则函数在性能分析工具中显示为 <Unknown>。
对于 Java 程序,收集器获取有关由 Java HotSpot 虚拟机编译的方法的信息,无需使用 API 函数提供信息。对于其他方法,性能工具显示有关执行方法的 JVM 软件的信息。在 Java 表示法中,所有方法都与已解释版本合并在一起。在机器表示法中,单独显示每个 HotSpot 编译的版本,且为每个已解释的方法显示 JVM 函数。
在某些情况下,PC 不会映射到已知函数。在这样的情况下,PC 会映射到名为 <Unknown> 的特殊函数。
以下情况显示映射到 <Unknown> 的 PC:
动态生成用 C 或 C++ 编写的函数,且未使用收集器 API 函数为收集器提供有关函数的信息时。有关收集器 API 函数的更多信息,请参见动态函数和模块。
动态编译 Java 方法但禁用 Java 程序分析时。
PC 对应于可执行文件或共享对象的数据段中的地址时。一种情况是 libc.so 的 SPARC V7 版本,在其数据段中有多个函数(例如,.mul 和 .div)。代码位于数据段中,以便库检测到该代码正在 SPARC V8 或 SPARC V9 平台上执行时可以动态重新编写该代码以使用机器指令。
PC 对应于在实验中未记录的可执行文件的地址空间中的共享对象时。
PC 不在任何已知的装入对象中时。最有可能的原因是展开失败,其中记录为 PC 的值根本不是 PC,而是某个其他字。如果 PC 是返回寄存器,并且看上去不在任何已知的装入对象中,则该 PC 会被忽略,而不是归属到 <Unknown> 函数。
PC 映射到收集器没有其符号信息的 JVM 软件的内部部分时。
<Unknown> 函数的调用方和被调用方表示调用堆栈中的上一个和下一个 PC,并以常规方式处理。
构造人工函数,并将其放置到用户模式调用堆栈(这些用户模式调用堆栈反映线程在 OpenMP 运行时库内处于某个状态的事件)。定义了以下人工函数:
|
在用户表示法中,<JVM-System> 函数表示 JVM 软件执行操作而不是运行 Java 程序所用的时间。在此时间间隔中,JVM 软件执行诸如垃圾收集和 HotSpot 编译之类的任务。缺省情况下,可在函数列表中看到 <JVM-System>。
<no Java callstack recorded> 函数类似于 <Unknown> 函数,但它仅用于 Java 表示法中的 Java 线程。当收集器从 Java 线程收到事件时,收集器会展开本机堆栈并调用到 JVM 软件中,以获取对应的 Java 堆栈。如果该调用由于任何原因而失败,则该事件会与人工函数 <no Java callstack recorded> 一起显示在分析器中。为了避免死锁,或展开 Java 堆栈时导致过度同步,JVM 软件可能会拒绝报告调用堆栈。
分析器为记录调用堆栈中各个函数的度量所用的缓冲区大小是有限的。如果调用堆栈大小变得如此大而导致缓冲区变满,则对调用堆栈大小的任何进一步增加都将强制分析器删除函数分析信息。由于在大多数程序中,大部分独占 CPU 时间用在叶函数中,因此分析器删除堆栈底部不太重要的函数(从入口函数 _start() 和 main() 开始)的度量。已删除函数的度量将合并到单个人工 <Truncated-stack> 函数中。<Truncated-stack> 函数也可能出现在 Java 程序中。
<Total> 函数是一个人工结构,用于将程序作为一个整体表示。除了归属到调用堆栈上的函数外,所有性能度量都归属到特殊函数 <Total>。该函数出现在函数列表的顶部,其数据可以用于为其他函数的数据提供透视。在“调用方-被调用方”列表中,它显示为所执行的任何程序的主线程中 _start() 的名义调用方,还显示为已创建线程的 _thread_start() 的名义调用方。如果堆栈展开是不完整的,则 <Total> 函数可能显示为 <Truncated-stack> 的调用方。
以下函数与硬件计数器溢出分析相关:
collector_not_program_related:计数器与程序不相关。
collector_hwcs_out_of_range:计数器似乎已超过溢出值,但未生成溢出信号。将会记录该值,并重置计数器。
collector_hwcs_frozen:计数器似乎已超过溢出值并被停止,但溢出信号似乎已丢失。将会记录该值,并重置计数器。
collector_hwc_ABORT:读取硬件计数器已失败(通常在特权进程已控制计数器时),导致硬件计数器收集终止。
collector_record_counter:处理和记录硬件计数器事件时累积的计数,一部分用于说明硬件计数器溢出分析开销。如果这对应于 <Total> 计数的重要部分,则建议使用较大的溢出间隔(即,较低的精度配置)。