Sun Studio 12:性能分析器

单线程执行和函数调用

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

将程序装入到内存中开始执行时,会为其建立上下文,包括要执行的初始地址、初始寄存器集和堆栈(用于存储临时数据和用于跟踪函数如何相互调用的内存区域)。初始地址始终位于函数 _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 或更高级别上。