Oracle Solaris Studio 12.2:性能分析器

第 2 章 性能数据

性能工具的工作方式是,在程序运行时记录有关特定事件的数据,然后将这些数据转换为程序性能的度量(称为度量)。度量可根据函数、源代码行和指令来显示。

本章介绍了通过性能工具收集的数据、如何处理和显示这些数据,以及如何使用这些数据进行性能分析。由于收集性能数据的工具有很多种,因此使用术语“收集器”来指代这些工具中的任何一种。同样,由于分析性能数据的工具也有很多种,因此使用术语“分析工具” 来指代这些工具中的任何一种。

本章包含以下主题。

有关收集和存储性能数据的信息,请参见第 3 章

收集器收集何种数据

收集器可收集三种不同类型的数据:分析数据、跟踪数据和全局数据。

分析数据和跟踪数据都包含有关特定事件的信息,并且这两种类型的数据都会转换为性能度量。全局数据不会转换为度量,而是用于提供标记器,这些标记器可用于将程序执行划分为很多时间段。通过全局数据,可以了解该时间段内程序执行的总体情况。

每个分析事件或跟踪事件收集的数据包都包含以下信息:

有关线程和轻量级进程的更多信息,请参见第 6 章

除了通用数据外,每个事件特定的数据包还包含特定于数据类型的信息。收集器可以记录的五种数据类型为:

以下几个小节将介绍这五种数据类型(度量即是根据这五种数据类型得出的),以及如何使用这些数据类型。第六种数据类型(全局抽样数据),无法转换为度量,因为它不包含调用栈信息。

时钟数据

进行基于时钟的分析时,收集的数据取决于操作系统所提供的度量。

Solaris OS 下基于时钟的分析

在 Solaris OS 下基于时钟的分析中,将按固定的时间间隔存储每个 LWP 的状态。这种时间间隔称为分析间隔。这些信息存储在一个整数数组中:数组的一个元素用于内核维护的十个微记帐状态中的每一个状态。收集的数据通过性能分析器转换为每个状态所用的时间和分析间隔的精度。缺省分析间隔约为 10 毫秒 (10 ms)。收集器提供的高精度分析间隔大约为 1 ms,低精度分析间隔大约为 100 ms,如果 OS 允许,则可使用任意的间隔。运行 collect 命令(不带任何参数)可列显运行该命令的系统所允许的范围和精度。

下表定义了从基于时钟的数据计算得来的度量。

表 2–1 Solaris 计时度量

度量 

定义 

用户 CPU 时间 

在 CPU 中按用户模式运行所用的 LWP 时间。 

挂钟时间 

LWP 1 中所用的 LWP 时间。该时间通常称为“挂钟时间”。 

总 LWP 时间 

全部 LWP 时间的总和。 

系统 CPU 时间 

在 CPU 中或陷阱状态下按内核模式运行所用的 LWP 时间。 

等待 CPU 时间 

等待 CPU 所用的 LWP 时间。 

用户锁定时间 

等待锁定所用的 LWP 时间。 

文本缺页时间 

等待文本页所用的 LWP 时间。 

数据缺页时间 

等待数据页所用的 LWP 时间。 

其他等待时间 

等待内核页所用的 LWP 时间,或休眠/ 停止所用的时间。 

对于多线程实验,将计算所有 LWP 的时间(挂钟时间除外)的总和。所定义的挂钟时间对于多程序多数据 (multiple-program multiple-data, MPMD) 程序没有意义。

计时度量按多种类别说明程序消耗时间的位置,并且可用于改善程序的性能。

Linux OS 下基于时钟的分析

在 Linux OS 下,唯一可用的度量是用户 CPU 时间。虽然报告的总 CPU 占用时间是准确的,但分析器不可能像在 Solaris OS 中那样准确地确定实际系统 CPU 时间的时间比例。虽然分析器显示的信息好像是轻量级进程 (lightweight process, LWP) 数据,但实际上 Linux OS 中没有 LWP 的数据;所显示的 LWP ID 实际上是线程 ID。

对 MPI 程序的基于时钟的分析

可以在用 Oracle Message Passing Toolkit(以前称为 Sun HPC ClusterTools)运行的 MPI 实验上收集时钟分析数据。Oracle Message Passing Toolkit 必须至少为版本 8.1。

如果将 Linux 与 Oracle Message Passing Toolkit 8.2 或 8.2.1 一起使用,可能另外需要一种解决方法。对于版本 8.1 或 8.2.1c,不需要解决方法;或者如果使用 Oracle Solaris Studio 编译器,对于任何版本都不需要使用解决方法。有关解决方法,请参见 docs.sun.com 上 Oracle Solaris Studio 12.2 Collection - Simplified Chinese 中的文档《Oracle Solaris Studio 12.2 的新增功能》。

在 MPI 实验上收集时钟分析数据时,可以显示两个其他度量:

在 Solaris OS 上,以串行或并行方式执行工作时,“MPI 工作”会累积。在以下情况下“MPI 等待”会累积:MPI 运行时正在等待进行同步时、该等待正在使用 CPU 时间或正在休眠时,以及正在以并行方式执行工作,但未在 CPU 上调度线程时。

在 Linux OS 上,仅当进程在用户模式或系统模式下处于活动状态时,“MPI 工作”和“MPI 等待”才会累积。除非您已指定 MPI 应执行忙等待,否则,Linux 上的“MPI 等待”将没有用处。

对 OpenMP 程序的基于时钟的分析

如果对 OpenMP 程序执行基于时钟的分析,将提供以下两种附加度量:“OpenMP 工作”和“OpenMP 等待”。

在 Solaris OS 上,以串行或并行方式执行工作时,“OpenMP 工作”会累积。在以下情况下“OpenMP 等待”会累积:OpenMP 运行时正在等待进行同步时、该等待正在使用 CPU 时间或正在休眠时,以及正在以并行方式执行工作,但未在 CPU 上安排线程时。

在 Linux OS 上,仅当进程在用户模式或系统模式下处于活动状态时,“OpenMP 工作”和“OpenMP 等待”才会累积。除非您已指定 OpenMP 应执行忙等待,否则,Linux 上的“OpenMP 等待”将没有用处。

硬件计数器溢出分析数据

硬件计数器可跟踪诸如高速缓存未命中次数、高速缓存停止周期、浮点运算、分支误预测、CPU 周期以及执行指令之类的事件。在硬件计数器溢出分析中,当运行 LWP 的 CPU 的指定硬件计数器溢出时,收集器会记录分析数据包。计数器将重置并继续进行计数。分析数据包中包括溢出值和计数器类型。

各种 CPU 系列支持同时存在二到十八个硬件计数器寄存器。收集器可收集一个或多个寄存器上的数据。对于每个寄存器,收集器都允许您选择计数器的类型来监视溢出,并设置计数器的溢出值。有些硬件计数器可以使用任意寄存器,而有些计数器仅可以使用特定的寄存器。因此,在一个实验中并非可以选择所有的硬件计数器组合。

硬件计数器溢出分析数据由性能分析器转换为计数度量。对于以循环方式计数的计数器,所报告的度量会转换为次数;而对于不以循环方式计数的计数器,所报告的度量为事件计数。在具有多个 CPU 的计算机上,用于转换度量的时钟频率为各个 CPU 时钟频率的调和平均数。因为每种类型的处理器都有其自己的一组硬件计数器,并且硬件计数器的数目庞大,因此,此处未列出硬件计数器的度量。下一小节讲述如何找出可用的硬件计数器。

硬件计数器的一个用途是可诊断进出 CPU 的信息流问题。例如,高速缓存未命中次数计数较高表明,重新组织程序的结构来改进数据或文本的位置或提高高速缓存的重用率可以改善程序性能。

某些硬件计数器与其他计数器相互关联。例如,分支误预测和指令高速缓存未命中次数通常是相关的,因为分支误预测会导致将错误的指令装入到指令高速缓存,而这些指令必须替换为正确的指令。这种替换会导致指令高速缓存未命中,或指令转换后备缓冲器 (instruction translation lookaside buffer, ITLB) 未命中,或甚至缺页。

通常会在导致事件和相应事件计数器溢出的指令之后,向硬件计数器溢出传送一条或多条指令:这称为“失控 (skid)”,它会使计数器溢出分析数据难以解释。如果缺少对精确识别因果指令的硬件支持,可以对候选的因果指令尝试合适的回溯搜索。

收集期间支持和指定这种回溯时,硬件计数器分析数据包还包括适用于硬件计数器事件的候选内存引用指令的 PC(program counter,程序计数器)和 EA(effective address,有效地址)。(在分析期间需要进行后续处理来验证候选事件 PC 和 EA)。关于内存引用事件的这一附加信息为各种面向数据的分析(又称为数据空间分析)提供了方便。仅在运行 Oracle Solaris 操作系统的基于 SPARC 的平台上支持回溯。

也可以为时钟分析指定候选事件 PC 和 EA 的回溯和记录,尽管这可能难以解释。

硬件计数器列表

由于硬件计数器是特定于处理器的,因此可以选用的计数器取决于所使用的处理器。性能工具为许多可能常用的计数器提供了别名。通过在特定系统上的终端窗口中键入不带任何参数的 collect,您可从收集器获得该系统上可用的硬件计数器列表。如果处理器和系统支持硬件计数器分析,则 collect 命令会列显两个包含有关硬件计数器信息的列表。第一个列表包含别名为通用名称的硬件计数器;第二个列表包含原始硬件计数器。如果性能计数器子系统和 collect 命令都不知道特定系统上的计数器名称,这些列表将为空。但是,在大多数情况下,可以用数值指定计数器。

以下示例显示了计数器列表中的条目。有别名的计数器将首先显示在列表中,然后是原始硬件计数器列表。该示例中的每一行输出都按打印格式显示。


Aliased HW counters available for profiling:
cycles[/{0|1}],9999991 (’CPU Cycles’, alias for Cycle_cnt; CPU-cycles)
insts[/{0|1}],9999991 (’Instructions Executed’, alias for Instr_cnt; events)
dcrm[/1],100003 (’D$ Read Misses’, alias for DC_rd_miss; load events)
...
Raw HW counters available for profiling:
Cycle_cnt[/{0|1}],1000003 (CPU-cycles)
Instr_cnt[/{0|1}],1000003 (events)
DC_rd[/0],1000003 (load events)

有别名的硬件计数器列表的格式

在有别名的硬件计数器列表中,第一个字段(例如,cycles)提供可以在 collect 命令的 -h counter... 参数中使用的别名。此别名还是在 er_print 命令中使用的标识符。

第二个字段列出计数器的可用寄存器,例如 [/{0|1}]

第三个字段(例如 9999991)是计数器的缺省溢出值。对于有别名的计数器,选择的缺省值可提供合理的样本率。由于实际样本率变化相当大,因此可能需要指定缺省值以外的值。

第四个字段(在圆括号中)包含类型信息。它提供简短描述(例如 CPU Cycles)、原始硬件计数器名称(例如 Cycle_cnt)以及计数单位类型(例如 CPU-cycles)。

如果类型信息的第一个单词是:

如果类型信息的第二个单词或仅有的单词是:

在示例中的有别名的硬件计数器列表中,类型信息包含一个单词的,如第一个计数器的 CPU-cycles 和第二个计数器的 events。类型信息包括两个单词的,如第三个计数器的 load events

原始硬件计数器列表的格式

原始硬件计数器列表中包含的信息是有别名的硬件计数器列表中信息的子集。原始硬件计数器列表中的每行包括由 cpu-track(1) 使用的内部计数器名称、可以在其上使用计数器的寄存器编号、缺省溢出值和计数器单位(可以是 CPU-cyclesEvents)。

如果计数器度量与运行的程序无关的事件,则类型信息的第一个单词是 not-program-related。对于这样的计数器,分析不会记录调用栈,而是显示人工函数 collector_not_program_related 中所用的时间。线程和 LWP ID 会被记录,但没有任何意义。

原始计数器的缺省溢出值为 1000003。对于大多数原始计数器来说,此值并非理想值,所以您应在指定原始计数器时指定溢出值。

同步等待跟踪数据

在多线程程序中,不同线程执行的任务同步会导致应用程序的执行延迟,例如,一个线程要访问已被其他线程锁定的数据时就不得不等待。这些事件称为同步延迟事件,并通过跟踪对 Solaris 或 pthread 线程函数的调用来收集这些事件。收集和记录这些事件的过程称为同步等待跟踪。等待锁定花费的时间称为等待时间

只有等待时间超过阈值(单位为微秒)时,才会记录事件。阈值为 0 表示跟踪所有的同步延迟事件,而不管等待时间为何。缺省阈值通过运行校准测试决定,在该测试中对线程库的调用不会出现任何同步延迟。阈值是这些调用的平均时间与某个因子(当前为 6)的乘积。该过程可防止对此类事件进行记录:即等待时间仅在于调用本身,而与实际的延迟无关。因此,数据量会大大减少,但同步事件的计数可能会被明显低估。

Java 程序不支持同步跟踪。

同步等待跟踪数据被转换为以下度量。

表 2–2 同步等待跟踪度量

度量 

定义 

同步延迟事件

对等待时间超过指定阈值的同步例程的调用数目。 

同步等待时间

超过指定阈值的等待时间的总和。 

通过该信息,您可以确定函数或装入对象对同步例程进行调用时是会经常被阻塞还是会经历很长时间的等待。高同步等待时间表示线程间的争用。您可以通过重新设计算法,尤其是重新组织锁的结构,以便仅包含需要锁定的每个线程的数据来减少争用。

堆跟踪(内存分配)数据

对未正确管理的内存分配和解除分配函数进行调用可能会造成数据的使用效率降低,从而导致应用程序的性能降低。在堆跟踪中,收集器通过插入 C 标准库内存分配函数 mallocreallocvallocmemalign 以及解除分配函数 free 跟踪内存分配和解除分配请求。对 mmap 的调用被视为内存分配,它允许记录 Java 内存分配的堆跟踪事件。Fortran 函数 allocatedeallocate 调用 C 标准库函数,因此会间接跟踪这些例程。

不支持对 Java 程序的堆分析。

堆跟踪数据会被转换为以下度量。

表 2–3 内存分配(堆跟踪)度量

度量 

定义 

分配数 

对内存分配函数的调用数。 

分配的字节 

每次调用内存分配函数时分配的字节总数。 

泄漏数 

调用内存分配函数(未对解除分配函数进行相应的调用)的数量。 

泄漏的字节 

已分配但未解除分配的字节数。 

收集堆跟踪数据有助于识别程序中的内存泄漏,或定位内存分配效率不高的位置。

内存泄漏的另一个常用定义(例如在 dbx 调试工具中)为:内存泄漏是一个动态分配的内存块,在程序的数据空间中没有任何指向它的指针。此处所使用的泄漏定义包括这种替换的定义,但也包括存在指针的内存。

MPI 跟踪数据

收集器可以收集有关对消息传递接口 (Message Passing Interface, MPI) 库的调用的数据。

使用开源 VampirTrace 5.5.3 发行版来实现 MPI 跟踪。该跟踪可识别以下 VampirTrace 环境变量:

VT_STACKS

控制是否在数据中记录调用堆栈。缺省设置为 1。将 VT_STACKS 设置为 0 将禁用调用栈。

VT_BUFFER_SIZE

控制 MPI API 跟踪收集器的内部缓冲区的大小。缺省值为 64M(64 兆字节)。

VT_MAX_FLUSHES

控制在终止 MPI 跟踪前刷新缓冲区的次数。缺省值是 0,这表示只要缓冲区满了就允许刷新到磁盘。将 VT_MAX_FLUSHES 设置为正数将为刷新缓冲区的次数设置限制。

VT_VERBOSE

启用各种错误和状态消息显示。缺省值为 1,在此情况下可打开紧急的错误和状态消息。如果出现问题,请将此变量设置为 2

有关这些变量的更多信息,请参见 Technische Universität Dresden Web 站点上的 Vampirtrace 用户手册。

在达到缓冲区限制之后发生的 MPI 事件将不会写入跟踪文件,这将导致跟踪不完整。

要去掉限制并获取应用程序的完整跟踪,请将 VT_MAX_FLUSHES 环境变量设置为 0。该设置将导致 MPI API 跟踪收集器在缓冲区已满时刷新磁盘的缓冲区。

要更改缓冲区大小,请设置 VT_BUFFER_SIZE 环境变量。该变量的最佳值取决于要跟踪的应用程序。设置较小的值将增加应用程序可以使用的内存,但是将触发 MPI API 跟踪收集器频繁进行缓冲区刷新。这些缓冲区刷新可能会显著改变应用程序的行为。另一方面,设置较大的值(如 2 G)可以使 MPI API 跟踪收集器刷新缓冲区的次数降至最低,但是将减少应用程序可以使用的内存。如果没有足够的内存可用来容纳缓冲区和应用程序数据,应用程序的某些部分可能会交换至磁盘,从而导致应用程序的行为发生显著改变。

下面列出了用于收集数据的函数

MPI_Abort

MPI_Accumulate

MPI_Address

MPI_Allgather

MPI_Allgatherv

MPI_Allreduce

MPI_Alltoall

MPI_Alltoallv

MPI_Alltoallw

MPI_Attr_delete

MPI_Attr_get

MPI_Attr_put

MPI_Barrier

MPI_Bcast

MPI_Bsend

MPI_Bsend-init

MPI_Buffer_attach

MPI_Buffer_detach

MPI_Cancel

MPI_Cart_coords

MPI_Cart_create

MPI_Cart_get

MPI_Cart_map

MPI_Cart_rank

MPI_Cart_shift

MPI_Cart_sub

MPI_Cartdim_get

MPI_Comm_compare

MPI_Comm_create

MPI_Comm_dup

MPI_Comm_free

MPI_Comm_group

MPI_Comm_rank

MPI_Comm_remote_group

MPI_Comm_remote_size

MPI_Comm_size

MPI_Comm_split

MPI_Comm_test_inter

MPI_Dims_create

MPI_Errhandler_create

MPI_Errhandler_free

MPI_Errhandler_get

MPI_Errhandler_set

MPI_Error_class

MPI_Error_string

MPI_File_close

MPI_File_delete

MPI_File_get_amode

MPI_File_get_atomicity

MPI_File_get_byte_offset

MPI_File_get_group

MPI_File_get_info

MPI_File_get_position

MPI_File_get_position_shared

MPI_File_get_size

MPI_File_get_type_extent

MPI_File_get_view

MPI_File_iread

MPI_File_iread_at

MPI_File_iread_shared

MPI_File_iwrite

MPI_File_iwrite_at

MPI_File_iwrite_shared

MPI_File_open

MPI_File_preallocate

MPI_File_read

MPI_File_read_all

MPI_File_read_all_begin

MPI_File_read_all_end

MPI_File_read_at

MPI_File_read_at_all

MPI_File_read_at_all_begin

MPI_File_read_at_all_end

MPI_File_read_ordered

MPI_File_read_ordered_begin

MPI_File_read_ordered_end

MPI_File_read_shared

MPI_File_seek

MPI_File_seek_shared

MPI_File_set_atomicity

MPI_File_set_info

MPI_File_set_size

MPI_File_set_view

MPI_File_sync

MPI_File_write

MPI_File_write_all

MPI_File_write_all_begin

MPI_File_write_all_end

MPI_File_write_at

MPI_File_write_at_all

MPI_File_write_at_all_begin

MPI_File_write_at_all_end

MPI_File_write_ordered

MPI_File_write_ordered_begin

MPI_File_write_ordered_end

MPI_File_write_shared

MPI_Finalize

MPI_Gather

MPI_Gatherv

MPI_Get

MPI_Get_count

MPI_Get_elements

MPI_Get_processor_name

MPI_Get_version

MPI_Graph_create

MPI_Graph_get

MPI_Graph_map

MPI_Graph_neighbors

MPI_Graph_neighbors_count

MPI_Graphdims_get

MPI_Group_compare

MPI_Group_difference

MPI_Group_excl

MPI_Group_free

MPI_Group_incl

MPI_Group_intersection

MPI_Group_rank

MPI_Group_size

MPI_Group_translate_ranks

MPI_Group_union

MPI_Ibsend

MPI_Init

MPI_Init_thread

MPI_Intercomm_create

MPI_Intercomm_merge

MPI_Irecv

MPI_Irsend

MPI_Isend

MPI_Issend

MPI_Keyval_create

MPI_Keyval_free

MPI_Op_create

MPI_Op_free

MPI_Pack

MPI_Pack_size

MPI_Probe

MPI_Put

MPI_Recv

MPI_Recv_init

MPI_Reduce

MPI_Reduce_scatter

MPI_Request_free

MPI_Rsend

MPI_rsend_init

MPI_Scan

MPI_Scatter

MPI_Scatterv

MPI_Send

MPI_Send_init

MPI_Sendrecv

MPI_Sendrecv_replace

MPI_Ssend

MPI_Ssend_init

MPI_Start

MPI_Startall

MPI_Test

MPI_Test_cancelled

MPI_Testall

MPI_Testany

MPI_Testsome

MPI_Topo_test

MPI_Type_commit

MPI_Type_contiguous

MPI_Type_extent

MPI_Type_free

MPI_Type_hindexed

MPI_Type_hvector

MPI_Type_indexed

MPI_Type_lb

MPI_Type_size

MPI_Type_struct

MPI_Type_ub

MPI_Type_vector

MPI_Unpack

MPI_Wait

MPI_Waitall

MPI_Waitany

MPI_Waitsome

MPI_Win_complete

MPI_Win_create

MPI_Win_fence

MPI_Win_free

MPI_Win_lock

MPI_Win_post

MPI_Win_start

MPI_Win_test

MPI_Win_unlock

   

MPI 跟踪数据会被转换为以下度量。

表 2–4 MPI 跟踪度量

度量 

定义 

MPI 发送数 

已启动 MPI 点对点发送数 

发送的 MPI 字节 

“MPI 发送”中的字节数 

MPI 接收数 

已完成 MPI 点对点接收数 

接收的 MPI 字节 

“MPI 接收”中的字节数 

MPI 时间 

对 MPI 函数的所有调用所花费的时间 

其他 MPI 事件 

对既没有发送也没有接收点对点消息的 MPI 函数的调用数 

MPI 时间是 MPI 函数中所用的总 LWP 时间。如果还收集了 MPI 状态时间,则除 MPI_Init 和 MPI_Finalize 之外的所有 MPI 函数的 MPI 工作时间加上 MPI 等待时间应大约等于 MPI 工作时间。在 Linux 上,MPI 等待和工作基于用户 CPU 时间加系统 CPU 时间,而 MPI 时间基于实际时间,所以这些数值将不匹配。

当前,仅针对点对点消息收集 MPI 字节和消息计数;不针对集体通信函数记录 MPI 字节和消息计数。“接收的 MPI 字节”度量会计算所有消息中接收的实际字节数。“发送的 MPI 字节”会计算所有消息中发送的实际字节数。“MPI 发送数”会计算发送的消息数,“MPI 接收数”会计算接收的消息数。

收集 MPI 跟踪数据有助于标识 MPI 程序中可能因 MPI 调用而产生性能问题的位置。可能发生的性能问题的例子有负载平衡、同步延迟和通信瓶颈。

全局(抽样)数据

全局数据由收集器以称为样本数据包的数据包来记录。每个包中都包含一个数据包头、时间戳、内核的执行统计数据(如缺页和 I/O 数据)、上下文切换以及各种页面驻留(工作集和分页)统计数据。记录在样本包中的数据对程序来说是全局的,且不会转换为性能度量。记录样本包的过程称为抽样。

在以下情况下,样本包会被记录下来:

性能工具使用记录在样本包中的数据,按时间周期将数据分组,这称为样本。您可以通过选择一组样本过滤特定于事件的数据,以便只查看这些特定时间周期的信息。您也可以查看每个样本的全局数据。

性能工具不对不同种类的样本点进行区分。要利用样本点进行分析,您应只选择一种点进行记录。具体地说,如果要记录与程序结构或执行序列有关的样本点,则应关闭定期抽样,并使用在 dbx 停止进程时,或将信号传送到正使用 collect 命令记录数据的进程时,或调用收集器 API 函数时记录的样本。

如何将度量分配到程序结构

使用与特定事件的数据一起记录的调用栈将度量分配到程序指令。如果该信息可用,则会将每条指令都映射到一行源代码,而分配到该指令的度量也被分配到该行源代码。有关如何完成此过程的更多详细说明,请参见第 6 章

除了源代码和指令,还会将度量分配到更高级别的对象:函数和装入对象。调用栈包含在执行分析时记录的有关函数调用到达指令地址的序列的信息。性能分析器使用调用栈来计算程序中每个函数的度量。这些度量称为函数级度量。

函数级度量:独占、包含和归属

性能分析器可计算三种类型的函数级度量:独占度量、非独占度量和归属度量。

对于只出现在调用堆栈底部的函数(叶函数),独占度量和非独占度量是相同的。

对于装入对象,也要计算独占度量和非独占度量。装入对象的独占度量通过累加装入对象中所有函数上函数级别的度量计算得出。装入对象的非独占度量与函数的非独占度量的计算方法相同。

函数的独占度量和非独占度量给出了有关所有通过函数记录的路径信息。归属度量给出了有关通过函数记录的特定路径的信息。这些度量显示了度量在多大程度上来自特定函数调用。调用中所涉及的两个函数称为调用方被调用方对于调用树中的每个函数:

各度量间的关系可通过以下等式表示:

显示各度量间关系的等式

通过比较调用方或被调用方的归属度量和非独占度量,可以得到以下进一步的信息:

要定位可改善程序性能的位置,请执行以下操作:

解释归属度量:示例

图 2–1 中说明了独占、包含和归属度量,该图包含完整的调用树。其中的焦点是中心函数,即函数 C。

图的后面显示了该程序的伪代码。

图 2–1 说明独占、包含和归属度量的调用树

说明独占、包含和归属度量的调用树。

Main 函数调用了函数 A 和函数 B,并将其 10 个单位的非独占度量归属到函数 A,将 20 个单位的非独占度量归属到函数 B。这些是函数 Main 的被调用方归属度量。它们的总和 (10+20) 加上函数 Main 的独占度量等于函数 main 的非独占度量 (32)。

由于函数 A 将其所有时间都花费在对函数 C 的调用上,因此它的独占度量为 0 个单位。

函数 C 由以下两个函数调用:函数 A 和函数 B,并将其 10 个单位的非独占度量归属到函数 A,将 15 个单位的非独占度量归属到函数 B。这些是调用方归属度量。它们的总和 (10+15) 等于函数 C 的非独占度量 (25)。

调用方归属度量等于函数 A 及 B 的非独占度量和独占度量之间的差额,这意味着这两个函数只调用函数 C。(实际上,这两个函数可能会调用其他函数,但时间太短,在实验中不显示。)

函数 C 调用了两个函数,函数 E 和函数 F,并分别将其 10 个单位的非独占度量归属到函数 E 和函数 F。这些是被调用方归属度量。它们的总和 (10+10) 加上函数 C 的独占度量 (5) 等于函数 C 的非独占度量 (25)。

函数 E 及函数 F 的被调用方归属度量和被调用方非独占度量是相同的。这意味着函数 E 及函数 F 仅由函数 C 调用。函数 E 的独占度量和非独占度量是相同的,但函数 F 的这两种度量不同。这是因为函数 F 调用了其他函数(函数 G),但函数 E 没有调用。

下面显示了该程序的伪代码。

    main() {
       A();
       /Do 2 units of work;/
       B();
    }

    A() {
       C(10);
    }

    B() {
       C(7.5);
       /Do 5 units of work;/
       C(7.5);
    }

    C(arg) {
          /Do a total of "arg" units of work, with 20% done in C itself,
          40% done by calling E, and 40% done by calling F./
    }

递归如何影响函数级度量

递归函数直接或间接的调用使得度量的计算复杂化。性能分析器将函数的度量作为一个整体显示,而不是显示函数的每个调用的度量:因此,必须将一系列递归调用的度量压缩为单一度量。这不会影响通过调用堆栈底部的函数(叶函数)计算得出的独占度量,但会影响非独占度量和归属度量。

非独占度量是通过将事件的度量添加到调用堆栈中函数的非独占度量来计算的。为了确保在递归调用堆栈中不重复计算度量,事件的度量仅能向每个唯一函数的非独占度量添加一次。

归属度量是通过非独占度量来计算的。在最简单的递归中,递归函数具有两个调用方:它本身和另一个函数(初始化函数)。如果在最后的调用中完成了所有工作,会将递归函数的非独占度量归属到它本身,而不是初始化函数。之所以发生此归属,是因为递归函数的所有更高调用的非独占度量均被视为零,以避免重复计算度量。但是,初始化函数会由于递归调用而作为被调用方正确归属到递归函数的非独占度量部分。