返回资讯中心
外部精选
软件工程
#开发者实时流

Rewind 虚拟机用分支计数器模拟时间,实现确定性重放

来源标题:A Clock Made of Branches

Rewind 确定性虚拟机通过维护两种时钟来保证重放一致性:退出时钟仅在客户机退出系统调用时推进 5 微秒,计数器时钟则按条件分支每条推进 1 纳秒。

Hacker News 最新setheron2026/11/2 · 今天18 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

在你想重放的机器里,时间是什么?🧘

构建 Rewind 这个确定性虚拟机时,你很快就会直面“如何表示时间”这个问题。你需要一种方法来测量 guest 内部的时间,以便在后续重放时,guest 看到相同的时间并做出相同的决策。时间是最根本的变量。

你不能简单记录宿主机的 wall-clock 时间,因为两个拦截点之间花费的时间取决于 guest 之外的因素,比如宿主机负载、CPU 频率、缓存和其他进程。因此,同样的 guest 工作在不同运行中可能花费不同的宿主机时间。

在“dafuq is a deterministic VM?”一文中,我解释了 Rewind VM 内部的时钟如何每步前进 5 微秒。更具体地说,这个时钟——exit time——只在 guest 退出时(比如因系统调用退出)才前进。它把程序在两次退出之间做的所有事情都视为瞬时完成。因此,一个 CPU 密集型的程序,比如在两次系统调用之间整整一秒只做数学运算,它看到的时间只过去了 5µs。🤦🏻‍♂️

Rewind 还有第二种时钟模式——counter time——它试图把工作量计算在内。guest 每退休一条条件分支,时钟就前进一纳秒。¹¹这与 rr 调试器推广的方法相同。rr 的论文称之为确定性时间(deterministic time)。

旁注: 刚接触 Rewind?请先阅读“a flaky build you only catch once”和“dafuq is a deterministic VM?”。 还是对“free”和“branches”的含义感到困惑?让我们看一个简单的例子来区分这两种时钟。

counter time 试图把工作量计算在内,而 exit time 只在退出时推进时钟。²²5µs 是我对当前硬件上系统调用和上下文切换合理开销的估计。1ns 大致是当前核心上一条条件分支的开销。

我们已经明白不能依赖宿主机的 wall-clock 时间。我们需要测量某种对同一程序的每次运行都相同的东西,以便将其等同于花费的时间。

guest 的指令流!CPU 是一台确定性机器:相同的输入、相同的寄存器、相同的内存、相同的指令,产生相同的下一个状态。

我们可以通过测量指令流中的进度来测量时间。💡

事实证明,CPU 制造商甚至为此提供了硬件计数器。它可靠吗?让我们来看看。🧐

让我们使用这个简单的程序:从十亿开始倒数。

int main(void) { long i = 1000000000; do { i--; } while (i != 0); return 0; _start: mov rcx, 1000000000 .loop: dec rcx jnz .loop mov eax, 60 ; exit(0) xor edi, edi syscall

对应的汇编包含一条 mov、十亿对 dec+jnz(条件分支),然后三条指令用于退出,总共 2,000,000,004 条指令。

让我们在 CPU 上运行两次,并用 perf stat 统计指令数和条件分支数:

$ nasm -felf64 loop.asm && ld -o loop loop.o $ taskset -c 5 perf stat -e instructions:u,ex_ret_cond:u ./loop 2,000,001,025 instructions:u 1,000,000,000 ex_ret_cond:u $ taskset -c 5 perf stat -e instructions:u,ex_ret_cond:u ./loop 2,000,001,054 instructions:u 1,000,000,000 ex_ret_cond:u

相同的程序和输入给出相同的条件分支计数,但指令数却相差一千左右。为什么?

尽管这个循环是 CPU 密集型的,且只递减一个寄存器,它仍然无法与外部世界的影响隔离开来。定时器在滴答,网络包在到达,其他核心在发送处理器间中断(IPI)。程序看不到这些,但我们探测的指令计数器能看到。

我将循环固定到单个 CPU 上,并从 /proc/interrupts 统计每次运行期间该 CPU 接收了多少中断。

500 1000 1500 0 500 1000 one extra per interrupt 500 1000 1500 interrupts on CPU 5 during the run extra counts (counted − executed) instructions conditional branches

虚线表示每次中断多一个计数,而指令计数器几乎完美地落在虚线上。³³拟合值为 0.94 而非 1,是因为 /proc/interrupts 也会捕获 perf 和 taskset 在循环运行前后启动和销毁期间发生的中断。 另一方面,无论发生多少中断,条件分支计数器都保持在零。

来自外部世界的中断并不是唯一能使指令计数产生偏差的因素。程序还可能发生缺页,这也是一种中断。指令计数器会看到内核绕道处理缺页,但程序本身看不到。

提示 开箱即用的情况下,AMD Zen 也会多计条件分支,在数千万条中多出几条,这源于一种名为 SpecLockMap 的推测优化。修复方法是在 PMU MSR 中设置一个特定的位,Rewind 会自动执行此操作,rr#2034 也是如此。

为了倒带历史,我们需要一种可靠且确定性的时间测量。我们传统的 wall-clock 时间由于外部世界的影响而不可靠,但我们确实有一种可靠的指令流进度测量:条件分支计数器。

使用条件分支计数器作为时间的代理,结果证明是一种相对不错的近似。

为什么我们关心拥有一种“现实”的时间表示?

许多程序读取时钟并据此做出决策,因此 guest 的时钟至少要在某种程度上是现实的。在 exit time 下,计算是“免费”的。一个检查某些工作“在 1 秒内完成”的测试总是通过。如果你追查的 bug 是超时,exit time 永远无法复现它。🙃

不现实的时钟也会使竞态产生偏差。在 exit time 下,一整串工作落在同一瞬间,这使线程挤在一起,因此竞态在 VM 中触发的频率可能远高于真实硬件上。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →