Instrumentation Profile 格式

概述

Clang 支持两种通过插桩进行 profiling 的类型 [1]:基于前端的和基于 IR 的,两者都可以支持各种用例 [2] 。本文档描述了两种二进制序列化格式(原始和索引)来存储插桩 profiles,特别强调 IRPGO 用例,因为当特定标头字段和负载部分在不同用例中具有不同的解释方式时,文档基于 IRPGO。

注意

前端生成的 profiles 与覆盖率映射一起用于基于源代码的代码覆盖率覆盖率映射格式与 profile 格式不同。

原始 Profile 格式

原始 profile 通过运行插桩后的二进制文件生成。来自可执行文件或共享库 [3] 的原始 profile 数据由标头和多个部分组成,每个部分都是内存转储。原始 profile 数据需要足够紧凑且快速生成。

原始 profile 格式没有向后或向前版本兼容性保证。也就是说,编译器和工具 require 需要特定的原始 profile 版本才能解析 profiles。

要将 profiles 反馈给编译器以进行优化构建(例如,通过 -fprofile-use 用于 IR 插桩),原始 profile 必须转换为索引格式。

通用存储布局

原始 profile 数据格式的存储布局如下所示。基本上,当原始 profile 被读取到内存缓冲区时,section 的实际字节偏移量是从 section 在布局中的顺序以及其前面所有 sections 的大小信息推断出来的。

+----+-----------------------+
|    |        Magic          |
|    +-----------------------+
|    |        Version        |
|    +-----------------------+
H    |   Size Info for       |
E    |      Section 1        |
A    +-----------------------+
D    |   Size Info for       |
E    |      Section 2        |
R    +-----------------------+
|    |          ...          |
|    +-----------------------+
|    |   Size Info for       |
|    |      Section N        |
+----+-----------------------+
P    |       Section 1       |
A    +-----------------------+
Y    |       Section 2       |
L    +-----------------------+
O    |          ...          |
A    +-----------------------+
D    |       Section N       |
+----+-----------------------+

注意

Sections 可能会被填充以满足特定的对齐要求。为了简洁起见,标头字段和仅用于填充目的的数据 sections 在上面的数据布局图中以及本文档的其余部分中被省略。

负载部分

二进制标识符

存储插桩二进制文件的二进制 ids,以将二进制文件与用于源代码覆盖率的 profiles 关联。有关设计,请参见 binary id RFC。

Profile 元数据

此 section 存储元数据,以将计数器和值 profiles 映射回插桩代码区域(例如,IRPGO 的 LLVM IR)。

元数据的内存表示形式是 __llvm_profile_data。某些字段用于引用 profile 中其他 sections 的数据。字段文档如下:

NameRef

函数 PGO 名称的 MD5 值。PGO 名称的格式为 [<filepath><delimiter>]<mangled-name>,其中 <filepath><delimiter> 是为本地链接函数提供的,以区分可能相同的函数。

FuncHash

函数 IR 的校验和,考虑了控制流图和插桩的值站点。有关详细信息,请参见 computeCFGHash

CounterPtr

Profile 数据与相应计数器起始位置之间的内存地址差。计数器位置以这种方式存储(作为链接时常量),与直接快照符号的地址相比,可以减小插桩二进制文件的大小。有关更多信息,请参见 commit a1532ed

注意

CounterPtr 对于非 IRPGO 用例可能表示不同的值。例如,对于 binary profile correlation,它表示计数器的绝对地址。如有疑问,请检查源代码。

BitmapPtr

Profile 数据与相应位图起始地址之间的内存地址差。

注意

CounterPtr 类似,此字段可能表示非 IRPGO 用例的不同值。

FunctionPointer

记录插桩二进制文件运行时函数地址。这用于在从原始 profile 转换为索引 profile 的过程中,将间接调用的已 profiling 的被调用者地址映射到 NameRef

Values

以二维数组形式表示值 profiles。第一维中的元素数量是所有类型中插桩的值站点的数量。第一维中的每个元素都是链表的头部,第二维中的每个元素都是链表元素,携带 <profiled-value, count> 作为有效载荷。编译器运行时在写出值 profiles 时使用它。

注意

值 profiling 受前端和 IR PGO 插桩支持,但并非在所有情况下都受支持(例如,lightweight instrumentation)。

NumCounters

插桩函数的计数器数量。

NumValueSites

这是一个计数器数组,每个计数器表示函数中某种类型的值的插桩站点数量。

NumBitmapBytes

函数的位图字节数。

Profile 计数器

对于 PGO [4],特定 FuncHash 的插桩函数中的计数器是连续存储的,并且顺序与插桩点选择一致。

如上所述,记录的计数器偏移量相对于 profile 元数据。那么函数计数器如何在原始 profile 数据中定位呢?

基本上,profile 读取器迭代 profile 元数据(来自 Profile 元数据 section),并利用记录的相对距离,如下所示。

       + --> start(__llvm_prf_data) --> +---------------------+ ------------+
       |                                |       Data 1        |             |
       |                                +---------------------+  =====||    |
       |                                |       Data 2        |       ||    |
       |                                +---------------------+       ||    |
       |                                |        ...          |       ||    |
Counter|                                +---------------------+       ||    |
 Delta |                                |       Data N        |       ||    |
       |                                +---------------------+       ||    |   CounterPtr1
       |                                                              ||    |
       |                                              CounterPtr2     ||    |
       |                                                              ||    |
       |                                                              ||    |
       + --> start(__llvm_prf_cnts) --> +---------------------+       ||    |
                                        |        ...          |       ||    |
                                        +---------------------+  -----||----+
                                        |    Counter for      |       ||
                                        |       Data 1        |       ||
                                        +---------------------+       ||
                                        |        ...          |       ||
                                        +---------------------+  =====||
                                        |    Counter for      |
                                        |       Data 2        |
                                        +---------------------+
                                        |        ...          |
                                        +---------------------+
                                        |    Counter for      |
                                        |       Data N        |
                                        +---------------------+

在图中,

  • profile 标头记录 CounterDelta,其值为 start(__llvm_prf_cnts) - start(__llvm_prf_data)。为了方便起见,我们将其称为 CounterDeltaInitVal

  • 对于每个 profile 数据记录 ProfileDataNCounterPtr 记录为 start(CounterN) - start(ProfileDataN),其中 ProfileDataN__llvm_prf_data 中的第 N 个条目,CounterN 表示相应的 profile 计数器。

每次读取器前进到下一个数据记录时,它都会 updates 更新 CounterDelta 以减去一个 ProfileData 的大小。

对于与第一个数据记录对应的计数器,相对于计数器 section 起始位置的字节偏移量计算为 CounterPtr1 - CounterDeltaInitVal。当 profile 读取器前进到第二个数据记录时,请注意 CounterDelta 已更新为 CounterDeltaInitVal - sizeof(ProfileData)。因此,相对于计数器 section 起始位置的字节偏移量计算为 CounterPtr2 - (CounterDeltaInitVal - sizeof(ProfileData))

位图

此 section 用于基于源代码的 Modified Condition/Decision Coverage 代码覆盖率。有关设计,请查看 Bitmap RFC

名称

此 section 包含函数 PGO 名称的可能压缩的串联字符串。如果压缩,则使用 zlib 库。

当原始 profiles 转换为索引 profiles 时,函数名称充当 PGO 数据哈希表中的键。它们对于 llvm-profdata 以人类可读的方式显示 profiles 也至关重要。

虚函数表 Profile 数据

此 section 用于 类型 profiling。每个条目对应于一个虚函数表,并由以下 C++ 结构定义

struct VTableProfData {
  // The start address of the vtable, collected at runtime.
  uint64_t StartAddress;
  // The byte size of the vtable. `StartAddress` and `ByteSize` specifies an address range to look up.
  uint32_t ByteSize;
  // The hash of vtable's (PGO) name
  uint64_t MD5HashOfName;
};

在 profile 使用时,编译器在排序的 vtable 地址范围内查找已 profiling 的地址,并通过哈希名称将地址映射到特定的 vtable。

虚函数表名称

此 section 与上面的 函数名称 section 类似,不同之处在于它包含已 profiling 的虚函数表的 PGO 名称。它是一个独立的 section,这样原始 profile 读取器可以通过访问相应的 profile 数据 section 直接找到每个名称集。

此 section 存储在原始 profiles 中,以便 llvm-profdata 可以以人类可读的方式显示 profiles。

值 Profile 数据

此 section 包含值 profiling 的 profile 数据。

与 profile 元数据对应的值 profiles 被连续序列化为一个记录,并且值 profile 记录以与各自 profile 数据相同的顺序存储,这样原始 profile 读取器可以 advances 同时前进 profile 数据的指针和值 profile 记录的指针 [5],以查找每个函数、每个 FuncHash profile 数据的 value profiles。

索引 Profile 格式

索引 profiles 由 llvm-profdata 生成。在索引 profiles 中,函数数据被组织为磁盘哈希表,以便编译器可以查找 IR 模块中函数的 profile 数据。

编译器和工具必须保持与索引 profiles 的向后兼容性。也就是说,在较新版本的代码上构建的工具或编译器必须理解由较旧的工具或编译器生成的 profiles。

通用存储布局

ASCII art 描绘了索引 profiles 的通用存储布局。具体来说,索引 profile 标头描述了各个负载部分的字节偏移量。

                +-----------------------+---+
                |        Magic          |   |
                +-----------------------+   |
                |        Version        |   |
                +-----------------------+   |
                |        HashType       |   H
                +-----------------------+   E
                |       Byte Offset     |   A
        +------ |      of section A     |   D
        |       +-----------------------+   E
        |       |       Byte Of fset    |   R
    +-----------|      of section B     |   |
    |   |       +-----------------------+   |
    |   |       |         ...           |   |
    |   |       +-----------------------+   |
    |   |       |      Byte Offset      |   |
+---------------|     of section Z      |   |
|   |   |       +-----------------------+---+
|   |   |       |    Profile Summary    |   |
|   |   |       +-----------------------+   P
|   |   +------>|      Section A        |   A
|   |           +-----------------------+   Y
|   +---------->|      Section B        |   L
|               +-----------------------+   O
|               |         ...           |   A
|               +-----------------------+   D
+-------------->|      Section Z        |   |
                +-----------------------+---+

注意

Profile 摘要 section 位于负载的开头。它紧随标头之后,因此在读取标头后其位置是隐式已知的。

标头

Header 结构是真理之源,结构字段应解释标头中的内容。在高层次上,*Offset 字段记录 section 字节偏移量,读取器使用这些偏移量来定位感兴趣的 sections 并跳过不感兴趣的 sections。

注意

为了保持索引 profiles 的向后兼容性,不应从结构定义中删除现有字段;不应修改字段顺序。应附加新字段。

负载部分

(CS) Profile 摘要

此 section 紧随 profile 标头之后。它存储序列化的 profile 摘要。对于上下文敏感的基于 IR 的插桩 PGO,此 section 存储与上下文敏感 profiles 相对应的附加 profile 摘要。

函数数据

此 section 将函数及其 profiling 数据存储为磁盘哈希表。具有相同名称的函数的 profile 数据被分组在一起并共享一个哈希表条目(例如,函数可能来自不同的共享库)。它们的 profile 数据被组织为键值对序列,其中键是 FuncHash,值是函数的 profiling 信息(由 InstrProfRecord 表示)。

MemProf Profile 数据

此 section 存储函数的内存 profiling 数据。有关设计,请参见 MemProf 二进制序列化格式 RFC

二进制标识符

此 section 用于从原始 profiles 携带 binary id 信息。

时间 Profile 追踪

此 section 用于从原始 profiles 携带时间 profile 信息。有关设计,请参见 temporal profiling

虚函数表名称

此 section 用于在索引 profile 中存储来自原始 profile 的 vtables 的名称。

与存储为 函数数据 哈希表键的函数名称不同,vtable 名称需要在索引 profiles 中存储在一个独立的 section 中。这样,llvm-profdata 可以以人类可读的方式显示已 profiling 的 vtable 信息。

Profile 数据用法

llvm-profdata 是用于显示和处理基于插桩的 profile 数据的命令行工具。对于支持的用法,请查看 llvm-profdata 文档