关注了就能看到更多这么棒的文章哦~
Data-type profiling for perf
December 21, 2023
This article was contributed by Julian Squires
ChatGPT translation
https://lwn.net/Articles/955709/
用于分析内存使用和排布效果的工具一直不如分析处理器 CPU 活动的工具那么好,因此,Namhyung Kim 的在 perf 中进行数据类型分析的补丁集是一个受欢迎的新改动。该补丁集提供了按数据类型聚合的内存访问细分信息,有助于了解 struct 的布局和访问模式的变化。现有的工具要么像heaptrack一样专注于分析内存分配,要么像 perf mem
一样仅在地址级别上记录内存访问。这项新工作是建立在后者的基础上,使用 DWARF 调试信息将内存操作与其源代码级别的数据类型关联起来。
最近的内核历史中充满了一些重新排序结构、填充字段或打包它们以提高性能的提交实例。但是,如何发现需要优化的结构并对它们的访问进行展示呢?Pahole提供了一个数据结构如何跨越 cache line 以及哪些位置是填充(padding)的静态展示,但无法展示任何关于访问模式的信息。perf c2c是一个用于识别缓存行竞争的强大工具,但对于单线程访问无法提供有用的信息。为了了解运行中程序的访问行为,需要更全面的数据结构访问展示功能。这就是 Kim 的数据类型分析工作发挥作用的地方。
以Ian Rogers 的最近对 perf 的更改 为例,他简述为:“避免填充的 6 字节空洞。试图在大多数情况下优先使用同一个缓存行。” 这是一种经典的采用结构重新排序进行的优化。Rogers 引用了在优化之前有关该结构的 pahole 输出:
struct callchain_list {
u64 ip; /* 0 8 */
struct map_symbol ms; /* 8 24 */
struct {
_Bool unfolded; /* 32 1 */
_Bool has_children; /* 33 1 */
}; /* 32 2 */
/* XXX 6 bytes hole, try to pack */
u64 branch_count; /* 40 8 */
u64 from_count; /* 48 8 */
u64 predicted_count; /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
u64 abort_count; /* 64 8 */
u64 cycles_count; /* 72 8 */
u64 iter_count; /* 80 8 */
u64 iter_cycles; /* 88 8 */
struct branch_type_stat * brtype_stat; /* 96 8 */
const char * srcline; /* 104 8 */
struct list_head list; /* 112 16 */
/* size: 128, cachelines: 2, members: 13 */
/* sum members: 122, holes: 1, sum holes: 6 */
};
我们可以看到这里存在一个空洞,并且整个结构跨越了两个缓存行(cache line),但除此之外,我们了解得并不多。Rogers 的补丁将 list_head
结构上移以填补这里所说的空洞,并同时将一个频繁访问的结构放入与其他经常使用的数据相同的缓存行。然而,进行这样的更改需要知道哪些字段最常被访问。这就是 perf 的新数据类型分析发挥作用的地方。
要使用它,首先通过以下命令对内存操作进行采样:
perf mem record
Intel、AMD 和 Arm 在其现代处理器上对记录精确内存事件有各自特别的一些支持,但这种支持在详尽程度上有所不同。在支持加载和存储分开进行 profiling 的处理器上(例如 Arm SPE 或 Intel PEBS),可以使用如下命令:
perf mem record -t store
来查找被大量写入的字段。在这里,我们将在 perf report
上使用它,并使用一个相当巨大的调用链来评估这个改动。
一旦用上述命令完成了一次运行,就可以使用生成的数据进行数据类型分析。Kim 的更改添加了一个新命令:
perf annotate --data-type
它会对每个 field 都打印所有 sample 的 struct;通过提供参数,可以将其缩小到单个类型。
perf annotate --data-type=callchain_list
在 Rogers 的补丁合入之前的输出如下,最活跃的字段以粗体突出显示:
Annotate type: 'struct callchain_list' in […]/tools/perf/perf (218 samples):==========================================================================
samples offset size field
218 0 128 struct callchain_list {
18 0 8 u64 ip;
157 8 24 struct map_symbol ms {
0 8 8 struct maps* maps;
60 16 8 struct map* map;
97 24 8 struct symbol* sym;
};
0 32 2 struct {
0 32 1 _Bool unfolded;
0 33 1 _Bool has_children;
};
0 40 8 u64 branch_count;
0 48 8 u64 from_count;
0 56 8 u64 predicted_count;
0 64 8 u64 abort_count;
0 72 8 u64 cycles_count;
0 80 8 u64 iter_count;
0 88 8 u64 iter_cycles;
0 96 8 struct branch_type_stat* brtype_stat;
0 104 8 char* srcline;
43 112 16 struct list_head list {
43 112 8 struct list_head* next;
0 120 8 struct list_head* prev;
};
};
这个 patch 的目的就能清晰看出来了。我们可以看到这个 list 是在第二个 cache line 里面由这个 workload 所访问到的唯一 field。如果它能被移到第一个 cache line 里的话,那么这个英语程序的 cache 表现就会更加好。我们可以用 data-type profiling 来验证这个假设,在加上了修改 patch 之后的输出如下:
Annotate type: 'struct callchain_list' in […]/tools/perf/perf (154 samples):==========================================================================
samples offset size field
154 0 128 struct callchain_list {
28 0 16 struct list_head list {
28 0 8 struct list_head* next;
0 8 8 struct list_head* prev;
};
9 16 8 u64 ip;
116 24 24 struct map_symbol ms {
1 24 8 struct maps* maps;
60 32 8 struct map* map;
55 40 8 struct symbol* sym;
};
1 48 8 char* srcline;
0 56 8 u64 branch_count;
0 64 8 u64 from_count;
0 72 8 u64 cycles_count;
0 80 8 u64 iter_count;
0 88 8 u64 iter_cycles;
0 96 8 struct branch_type_stat* brtype_stat;
0 104 8 u64 predicted_count;
0 112 8 u64 abort_count;
0 120 2 struct {
0 120 1 _Bool unfolded;
0 121 1 _Bool has_children;
};
};
至少对于这个工作场景来说,访问模式确实如之前所告知的。进行一些快速的 perf stat
性能基准测试,就可以看到指令每周期计数增加,并且消耗的时间在减少。
任何花费大量时间仔细研究 pahole 输出、试图调整结构成员来权衡大小、缓存行访问、伪共享(false sharing)等的人可能会看到这个方案的价值。(尚未深入研究过这方面知识的读者可能希望从 Ulrich Drepper 在 LWN 上的系列文章开始:"程序员应该了解的关于内存的一切",特别是第 5 部分,"程序员可以做些什么"。)
数据类型分析显然需要有关其正在查看的程序的信息,以便能够完成其工作;具体而言,识别与 load 或 store 相关联的数据类型需要有关位置、变量和类型的 DWARF 调试信息。perf 支持的任何语言都应该可以工作。作者验证了,除了 C 语言外,Rust 和 Go 程序产生的输出也是合理的,尽管不总是符合相关语言的惯例。
在对内存访问进行采样后,数据类型聚合将采样的指令参数与相关 DWARF 信息中的位置进行关联,然后与其类型进行关联。在性能分析中,编译器优化通常会阻碍这种搜索。不幸的是,这意味着在某些情况下,perf 无法将内存事件与类型关联起来,因为 DWARF 信息要么不够详细,要么对于 perf 来说过于复杂以至于无法解释。
Kim 在 2023 年的 Linux Plumbers Conference 上讲解过这项工作(有视频),并指出链式指针的情况是一个常见情况,并且目前支持还不好。虽然他为这个问题提供了一种解决方法,但他还指出,有一份关于 DWARF 中反转位置列表(inverted location lists)的提案,将是一个更通用的解决方案。
对于给定的程序地址(通常是当前程序计数器(PC)),DWARF 中的位置列表可以让调试工具查找符号当前存储的方式;它可以是一个位置描述,可能指示 symbol 当前存储在寄存器中,也可以是一个地址。像 perf 这样的工具实际上更希望有一个从地址或寄存器到 symbol 的映射。这实际上就是位置列表的反向使用,但对于生成调试信息的编译器来说,计算这种反转要轻松高效得多。从 Arnaldo Carvalho de Melo 在 Linux Plumbers Conference 2022 的演讲中可以看出,这在过去一直是 perf 的一个敏感问题(有视频)。
截至本文,Kim 的工作尚未合并,但由于更改仅在用户空间,因此可以通过构建从 Kim 的 perf/data-profile-v3
分支的 perf 来简单进行尝试。鉴于来自 perf 工具维护者 Arnaldo Carvalho de Melo、Peter Zijlstra 以及 Ingo Molnar 对来自 v1 补丁集的热烈反应,似乎它不用很久就可以合入了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~