Xperf(Windows Performance Toolkit,也称为 ETW)是一个强大的性能分析工具,但它的使用颇具挑战性。部分难度来自其本身的复杂度——例如,如果你想深入调查线程调度问题,你需要全面理解 Windows 线程调度器的工作原理。
这篇文章在 2015 年 9 月进行了更新,添加了与 UIforETW、WPA 及新列相关的信息。
然而,另一些难度则来自“附带的”、也可以说是不必要的复杂度,其中之一就是对摘要表中各列的意义没有任何文档说明。这些列代表的数据非常具体且细微,但是缺乏文档就意味着 xperf 的使用者在真正开始分析性能问题之前,必须先自己推断列的含义、在脑海中构建模型,甚至在某些情况下还要自己想办法弄清楚某些列的单位!
本系列中的其他文章:
在本文中,我将尝试记录 CPU Usage (Sampled) 摘要表中的各列。这个表显示了 CPU 采样信息,让你可以查明CPU 时间的消耗位置。

本文假设你使用 UIforETW 来记录追踪数据(trace),并且已经安装了 UIforETW 自带的 WPA(Windows Performance Analyzer)启动配置文件。你可以在设置对话框里点击 Copy startup profiles 来完成此操作。本文还假设你使用 WPA 10 来查看追踪数据——当你在 UIforETW 的 Traces 列表中双击某个追踪文件时,这通常就是默认的查看方式。
CPU Usage (Sampled)
Xperf 内置了一个优秀的基于采样的分析器,其默认采样率是 1 KHz(但最高可以到 8 KHz——只需在 UIforETW 中勾选 Fast sampling 即可)。xperf 采样器会以极低的开销抓取系统上下文中的调用栈(包括内核和用户模式),大大优于各种开源项目里那些基于 SuspendThread/ResumeThread 的采样方法。
UIforETW 默认的启动配置文件将 CPU Usage (Sampled) 数据呈现在表格中。虽然也可以以图表形式查看采样数据,但因为 CPU Usage (Precise) 图表的准确度更高(它基于上下文切换),所以我选择用精准图表(基于上下文切换的)来展示 CPU 占用情况,而把 CPU Usage (Sampled) 放在表格里。
默认列
在 UIforETW 的启动配置文件中,默认的视图预设(View Preset)名为 “Randomascii inclusive (stack)” ,包含以下列:

- Process
- Thread ID
- Stack
- 橙色栏(Orange bar,用于分组的分割标识)
- Count
- Weight (in view) (ms)
如果你想按进程分组,可以隐藏 Thread ID 列。如果你想按进程名(而不是进程 ID)分组,则可以隐藏 Process 列并启用 Process Name 列。只要你按 Count 或 Weight (in view) 列进行排序,那么最消耗 CPU 的调用栈就会排在最上面。
另一个有用的 “视图预设” 名为 “Randomascii exclusive (module and function)” ,它包含以下列:

- Process
- Thread ID
- Module
- Function
- 橙色栏(Orange bar)
- Count
- Weight (in view) (ms)
“包容式(inclusive)”视图最适合通过调用栈(call stack)来归因采样结果(即找出“谁调用了那个昂贵的函数”)。而“独占式(exclusive)”视图则更适合查明单个函数本身消耗了多少 CPU 时间。有时,为 “独占式” 视图加上 Address 列也会很有帮助,可以用来查看函数内部哪些指令发生了采样。只要仍然通过 Count 或 Weight 列排序,CPU 采样最多的线程就会在顶部,而在该线程之下,采样最多的模块也会排在前面,依此类推。
需要注意的是,“当定时器中断触发时,哪条指令在执行” 这个概念对于拥有数十条指令在流水线中同时执行的超标量、乱序处理器来说并不那么简单。关于如何理解 Address 列里记录的地址,我见过最好的分析在此处。
当然还有一些其他的列组合可能有用,但大多数摘要表格问题都可以通过 “把列调整成这样……” 来回答,这里就不再展开。等你领悟了摘要表的 “禅意(Zen)”,就能自行判断何时使用哪些列了。
列的文档
我们假设你已经阅读了如何记录追踪数据(trace) 的文章,并且手头有可供分析的追踪文件。
采样摘要表中的所有列都和某个或多个采样关联在一起,并且分组方式(即放在橙色栏左侧的列)会决定显示多少采样、生成多少行。默认视图 “Randomascii inclusive” 会为每个进程生成一行数据。当你往下展开时,就会显示更多行,而按调用栈(Stack)进行分组的方式尤为重要,可以让你清晰地看到全局情况。
只要在当前可视时间范围内没有被采样到的线程或进程,就不会出现在表中,即使它们确实短暂运行过。
以下是一些重要列的解释:
- Process Name 表示某个采样所属的进程名(例如 devenv.exe),不包含进程 ID。当你想把同一个可执行文件(如多个 devenv.exe 进程)产生的采样合并在一起时,就可以用它来分组。
- Process 包含进程名和进程 ID。当你想让每个进程单独分组时,它就很有用。
- Stack 动态展开查看所有采样对应的调用栈数据。只有当该列位于橙色栏左侧时它才有用。该列表示包容式时间:如果在某个函数或它的子函数(经由该栈路径)出现了采样,就会在这个函数中显示采样。
- Module 采样击中的模块。该列表示独占式时间。如果已经使用了 Stack 列,则通常很少再用到它。
- Function 采样击中的函数。该列表示独占式时间,通常与 Module 列配合使用。
- Address 采样命中的指令指针地址。该列表示独占式时间,常配合 Module 和/或 Function 列一起使用。
- Thread ID 采样所属的线程 ID。如果你启用了此列并把它放在 Process 和 Stack 之间,那么就会先按线程(在各个进程内部)分组,从而可以单独分析每个线程。
- Weight (in view) 当前分组这一行对应的 CPU 时间估计值(毫秒)。由于采样分析器本质上不知道采样之间发生了什么,它只能做统计学估计。如果采样数量足够并且代码没有与定时器同步,那么就会相对准确。
- %Weight 估计当前分组耗费的 CPU 时间占所有可用 CPU 时间的百分比。如果你的机器是八核(或四核八线程),那么 100% 表示所有线程在所有时间都在运行,而 12.5% 表示有一个线程一直在运行。在多核机器上,这个值的直观意义会弱一些,所以通常用处不大。
- Count 记录了采样次数。在默认采样率(1 KHz)下,这个数字会与 Weight(毫秒数)非常接近,但如果采样率改变了,就无法与毫秒数直接对应。因此,若想估算 CPU 占用,最好还是使用 Weight。不过 Count 的主要作用是让你判断采样次数是否具有统计意义(是否足够多)。
- TimeStamp 采样发生时的时间戳,单位是秒,具有纳秒级精度。有时可以通过按时间排序(通常需要分组方式为按采样逐行展开)来查看采样的时间分布,但这种情况并不常见。
- Annotation 这个列在大多数或全部 WPA 表格中都可以使用,并且可以在分析时进行手动注释。详情可参见这篇文章。

大多数常用列可以通过右键单击表头并勾选相应列名来启用。一些比较 “偏门” 的列(如 Image RVA, Is PGO’ed, Compiler Optimization)则需要在 View Editor 中,从左侧的 Available Columns 拖拽到右侧的列配置中才能看到。
一般不太重要的列
以下是通常不太重要的列(为尽量全面,这里也加以说明):
- Display Name 通常就是进程名;对于服务进程,则可能是传给 CreateService 的 lpDisplayName 参数。
- Thread Start Module CreateThread 传入的函数所位于的模块。
- Thread Start Function 传给 CreateThread 的函数名。
- DPC/ISR 指示采样是出现在常规 CPU 使用时段、DPC 还是 ISR。如果你怀疑过多的驱动程序耗时是瓶颈,你可以启用此列并把它拖到左边进行分组。但如果你只是关注常规 CPU 使用,通常不需要它。
- CPU 采样记录时线程所在的 CPU 编号。除非你在研究操作系统线程调度器,一般不用关心它。需要注意:WPA 10 对此列的工具提示有误。
- Priority 线程在采样时的优先级。
- Table 固定值 “CPU Usage (Sampled)”。
- Section Name 表示样本命中的可执行文件 PE(Portable Executable)段名,通常是 “.text”。
- Section RVA 相对于所在 PE 段起始位置的偏移地址。
- Image RVA 相对于 DLL 或 EXE 文件开头的偏移地址。
- Compiler Optimization 显示代码编译时使用的优化级别(如 “Speed” 或 “Size”)。需要留意的是,这个值不一定可信:例如,手写汇编可能会显示成 “Size”,而不优化的 Debug 模式 C++ 代码则可能显示成 “Speed”。理论上,你可以用它来确认那些热点函数是否确实被优化到了最快模式。
- Inlined Functions 显示哪些函数被内联到另一个函数中。也可能很重要,但由于 WPA 处理 Chrome 的私有符号速度非常慢,我总会把它们剥离掉,因此无法看到这些信息。如果有人觉得它非常有用,请告诉我。
- Trace # 如果 WPA 同时加载了多个追踪文件,此列指示当前行属于哪一个追踪文件。
不明或尚不清楚的列
- Stack Tag 和 Stack (Frame Tags) 用于给栈帧加上附加信息,微软对其有简要说明。WPA 自带一些示例“标签”文件,可在 “C:\\Program Files (x86)\\Windows Kits\\10\\Windows Performance Toolkit\\Catalog\\default.stacktags” 找到。
- Thread Name, Thread Activity Tag 不清楚其含义,似乎未被使用。
- Source File name, Source Line Number 看起来很有前途,但似乎从未实际显示过任何数据。
- Rank 含义不明。在我的 Windows 10 测试中显示了 –1 到 2 之间的值。
- 还有一些与 PGO(性能指导优化)相关的列,有时只能在 View Editor 里打开。更多信息可在 这篇文章(作者:Kyle Sabo)中找到。
另请参考这篇文章,它讨论了在乱序、超标量处理器上如何看待采样所对应的指令位置。
关于 brucedawson
我是 Google 的一名程序员,主要工作是做优化和可靠性开发。让代码跑得快 10 倍比什么都开心,除非能同时消灭大量 bug。我也玩单轮车、打冰球(ice hockey)和轮椅冰球(sled hockey),还会玩杂耍(juggle)。有时我会纠结这个博客究竟该不该叫 randomutf-8。了解更多可看我在 Twitter 上的2010 年代回顾。
以上翻译仅供参考,原文信息请以英文原版为准。
原文链接:https://randomascii.wordpress.com/2012/05/08/the-lost-xperf-documentationcpu-sampling/
2