Wall-Clock 与 CPU-Cycles 采样的区别

我曾写过一篇 深入探索 perf CPU Profiling 实现原理 的文章,希望能帮助大家理解这个强大工具背后的运作机制。在那篇文章里,我尽可能地拆解了从中断到调用栈还原的整个流程。
然而,在后续的技术学习和思考过程中,我意识到其中一个核心概念的阐述,虽然看似合理,却隐含着一个重要且易被误解的核心概念。今天,我想写下这篇“勘误与续篇”,与大家一同分享这个让我醍醐灌顶的“Aha!”时刻,重新探讨两种采样方式:Wall-Clock 采样与 CPU-Cycles 采样。
最初的误解:CPU 的节拍是恒定的吗?
在我之前的文章中,关于为何采样 cycles
事件能分析 CPU 性能,我是这样描述的:
“每经过一个 CPU 周期都会触发一个 cycles 事件。可以认为,cycles 事件是均匀的分布在程序的执行期间。这样,以固定频率去采样的 cycles 事件,也是均匀的分布在程序的执行期间。我们在采样 cycles 事件时,记录 CPU 正在干什么,持续一段时间收集到多个采样后,我们就能基于这些信息分析程序的行为...”
这个逻辑听起来是不是无懈可击?一个 4 GHz 的 CPU,每秒不就是产生 40 亿个时钟周期(cycles)吗?那么按 cycles
采样不就等同于按一个极其精确的时间间隔采样吗?
正是这个看似合理的假设,让我走入了一个误区。 事实是,CPU 远比我们想象的要“聪明”和“懒惰”。
墙上时钟采样(Wall-Clock Sampling)
为了理解 cpu-cycles
采样的精妙之处,我们首先要看它的参照物:墙上时钟采样(Wall-Clock Sampling)。
你可以把墙上时钟采样想象成一个厨房定时器。你设定它每 1 分钟响一次,它就会忠实地每分钟提醒你一次,完全不管你是在切菜、炒菜,还是在刷手机,等水烧开。
这种采样方式依赖一个全局的、按真实时间流逝的计时器。比如,你设定每 10 毫秒采样一次,操作系统就会每隔 10 毫秒触发一次中断,然后去看:“嘿,CPU 核心此刻正在忙什么?”
这种方式的核心是对时间点的快照,它并不关心两次快照之间发生了什么。让我们从 CPU 的视角来看一个场景:
假设在 100 毫秒的总时间内,一个 CPU 核心的活动如下:
0-20ms: 运行高强度的计算任务(进程 A)。
20-80ms: 系统无事可做,CPU 进入空闲(Idle)状态。进程 A 正在等待网络数据,而系统中没有其他需要计算的任务。
80-100ms: 计算任务(进程 A)被唤醒,继续在 CPU 上运行。
现在,我们用一个每 10 毫秒一次的墙上时钟采样器来观察这个 CPU 核心。它总共会进行 10 次采样,采样点大约会落在 10ms, 20ms, 30ms, ..., 100ms 的时刻。
在 10ms 和 20ms 时刻,采样器会看到 CPU 正在执行进程 A。
在 30ms, 40ms, 50ms, 60ms, 70ms 时刻,采样器会看到 CPU 正在执行一个特殊的空闲任务(Idle Task)。
在 80ms, 90ms, 100ms 时刻,采样器又会看到 CPU 在执行进程 A。
最终的分析报告会告诉你,在大约 50% 的采样点上,CPU 在执行进程 A,而在另外 50% 的采样点上,CPU 处于空闲状态。

这个结果本身是准确的,它忠实地反映了 CPU 在总时间内的状态分布。但 Wall-Clock 采样方式,将“计算”和“等待导致的空闲”混在了一起。如果你想找出是什么消耗了最多的 CPU 计算资源,这种采样方式可能会给你带来困惑,因为它花费了大量的样本去记录“什么都没在发生”的空闲时刻。
想起一个流传已久的性能分析领域的笑话:一位程序员使用性能分析器后发现,有一个进程占用了大量的 CPU 时间。于是他花了很大力气进行优化,但程序的运行速度并没有变快。原来被他优化这个进程,其实是“idle loop”,也就是没有其他工作可做时才会运行的部分。
CPU-Cycles 采样:只关心“干活”的计步器
现在,让我们揭开 cpu-cycles
采样的真正面纱。
“一个 4 GHz 的 CPU,每秒产生 40 亿个 cycles”这个描述是不准确的。 它描述的是 CPU 的最高性能,而不是恒定状态。
正确的理解是:现代 CPU 为了节能和控制温度,会动态地调整自己的状态。
当无事可做时(Idle):CPU 会进入深度睡眠(C-states),此时它的时钟频率极低,甚至可能完全停摆。在这期间,它几乎不产生任何
cpu-cycles
。当任务清闲时(Light Load):它会降低自己的工作频率(比如从 4 GHz 降到 1 GHz)来省电。
当任务繁重时(Heavy Load):它才会火力全开,提升到标称频率甚至超频来快速完成工作。
现在,cpu-cycles
的真正含义就清晰了:它不是时间的度量,而是 CPU 完成工作的度量单位。
cpu-cycles
采样就像一个只在你跑步时才计数的计步器。当你坐下休息时,计步器是完全静止的。同理,当 CPU 进入空闲状态时,为它计数的 cpu-cycles
计数器也随之暂停了。
操作系统会与 CPU 的性能监控单元(PMU)协作,设定一个阈值,当累计消耗的 CPU Cycles 达到了预设值(比如 3000 万个周期)时,才会触发一次采样中断。

这种采样方式保证了每一次采样,都必然命中了一个真正在消耗 CPU 资源、在“干活”的线程。一个线程被采样的次数,与它消耗的 CPU 总量成正比。
CPU-Cycles vs. Wall-Clock
在分别了解了 Wall-Clock 和 CPU-Cycles 采样之后,我们可能会有一个疑问:它们到底哪个更好?其实,这是一个关乎视角的问题,而非优劣之分。
当 CPU 被 100% 占满、满负荷运转时,无论是 Wall-Clock 采样还是 cpu-cycles 采样,它们看到的结果会非常相似,都是 CPU 在忙于执行代码的快照。

然而,真正的区分出现在 CPU 并非 100% 繁忙的时候。这两种方法最根本的区别,在于它们如何看待 CPU 的空闲(Idle)时间。
Wall-Clock 采样,作为一个时间的忠实记录者,它会一视同仁地记录下所有状态,无论是繁忙的计算,还是无所事事的等待。
CPU-Cycles 采样,则像一个只关心产出的质检员。如果生产线(CPU)停了,它也跟着停下休息,完全不关心停了多久。它只在指令执行时才进行工作。
这种看待问题的不同视角,在性能分析领域有着专门的术语:On-CPU 分析和 Off-CPU 分析。
On-CPU 分析关注的是:“是什么让我的 CPU 如此忙碌?”它的目标是找到消耗计算资源最多的代码热点。
Off-CPU 分析关注的是:“我的程序为什么在等待,而不是在运行?”它的目标是找到那些导致程序停滞的瓶颈,如 I/O 等待、锁竞争、或者休眠。
这两种分析方法,也对应着我们最关心的两个核心性能指标:吞吐量(Throughput)和延迟(Latency)。
CPU-Cycles 采样,通过聚焦 On-CPU 时间,帮助我们提升程序的计算效率。这直接关系到吞吐量,即单位时间内能处理多少工作。优化掉一个 CPU 热点,意味着每个任务消耗的 CPU 时间更少,服务器自然能承载更多的请求。
Wall-Clock 采样,通过完整地展现包括 Off-CPU 在内的全部时间,帮助我们理解和优化程序的响应速度。这直接关系到延迟,即完成单个任务需要多长时间。如果一个请求 99% 的时间都在等待数据库返回结果,那么优化计算逻辑对降低延迟几乎没有帮助。
我们应该如何选择呢?答案是:取决于你想要解决的问题。
如果你想优化一个计算密集型服务,降低服务器成本,或者想找出算法中的性能瓶颈,那么 CPU-Cycles 采样是你的不二之选。它是性能优化的“手术刀”,精准且致命。
但如果你遇到的问题是“我的程序启动为什么这么慢?”,情况就完全不同了。应用程序的启动过程,是一个混合了大量磁盘 I/O(读取配置文件和库)、网络 I/O(连接数据库或服务)以及 CPU 计算的复杂过程。在这种场景下,Wall-Clock 采样会非常有用。它能为你绘制一幅完整的启动时间线,清晰地标出那些漫长的“等待”鸿沟,让你知道时间到底被浪费在了哪里。同理,在调试锁竞争或分析外部服务调用延迟时,它也是一把利器。
总而言之,这两种采样方式没有绝对的优劣,它们像是性能分析工具箱里两种不同用途的工具,为我们提供了观察系统的不同维度。
perf record -F 99
的真正魔法
理解了上述区别,我们才能真正领会 perf record -F 99
命令背后的原理。
它并不是简单地“每秒采样 99 次”,背后是内核与硬件精巧的协作:
目标驱动:
-F 99
告诉内核:“我的目标是在每个 CPU 核心上,都达到平均每秒 99 次的采样频率。”动态计算:内核不是设定一个固定的
cpu-cycles
间隔。相反,它会通过cpufreq
子系统或 MSR 寄存器,实时地查询到 CPU 当前的工作频率。智能调整:
如果 CPU 正以 4 GHz 的高频率运行,内核会计算出一个较大的
cpu-cycles
间隔(比如4,000,000,000 / 99
),然后设置硬件计数器。如果 CPU 因为负载低而降频到 1 GHz,内核会立刻感知到,并自动将采样间隔调整为一个较小的
cpu-cycles
数值(比如1,000,000,000 / 99
)。
cpu-cycles
的采样周期是动态变化的,但最终达成“每秒采样 99 次”这个目标。
这种机制带来了巨大的好处:
它只在 CPU 忙碌时采样,天然地过滤掉了所有 I/O 等待和空闲时间造成的噪音。
它自适应 CPU 的频率变化,确保了分析结果在不同负载下的一致性。
它在不同硬件上表现一致,你在笔记本和在服务器上使用
-F 99
,得到的都是相似密度的有效数据。
总结
这次对性能分析的重新探索,始于一个看似简单的问题:“为何采样 cycles 事件能分析 CPU 的性能?”我们最初的假设是,cycles 在时间上是均匀的,采样它就如同按时间采样。
而经过这趟深入的旅程,我们发现:CPU-Cycles 采样并非时间的度量,而是 CPU 完成工作量的度量。
当 CPU 因为等待 I/O 或无事可做而进入休眠时,它的 cpu-cycles
计数器也随之暂停。只有当代码真正在 CPU 上执行指令时,cycles 才会产生。这意味着,一个函数被 cycles 采样命中的次数,与它流逝了多少时间无关,而是与其消耗了多少实际的 CPU 计算资源成正比。CPU-Cycles 采样的结果反映了程序在 CPU 繁忙时间(Busy Time)中的资源消耗分布,是分析 On-CPU 问题、优化吞吐量(Throughput) 的“手术刀”。
Wall-Clock 采样以恒定的时间间隔为基准,它捕获的是在特定时间点上 CPU 的状态。其结果反映了程序在总流逝时间(Total Time) 中的状态分布,天然地包含了 On-CPU 和 Off-CPU(如 Idle, I/O Wait)两个维度,是分析延迟(Latency) 问题的利器。
这次“勘误”,不仅修正了我之前的文章,也使我对性能分析的底层原理有了一个更清晰的理解,希望对你也有帮助。
版权声明: 本文为 InfoQ 作者【mazhen】的原创文章。
原文链接:【http://xie.infoq.cn/article/5affdbedc16f250cc16c0dd20】。文章转载请联系作者。
评论