rdtscp 的“半围栏”行为是怎么回事?

Bee*_*ope 5 performance x86 assembly microbenchmark rdtsc

多年来,x86 CPU 都支持该rdtsc指令,该指令读取当前 CPU 的“时间戳计数器”。这个计数器的确切定义随着时间的推移而改变,但在最近的 CPU 上,它是一个相对于挂钟时间以固定频率递增的计数器,因此它作为快速、准确时钟或测量时间的构建块非常有用由小段代码获取。

关于rdtsc指令的一个重要事实没有以任何特殊方式与周围的代码一起排序。像大多数指令一样,它可以相对于与它没有依赖关系的其他指令自由地重新排序。这实际上是“正常的”,对于大多数指令,它只是一种使 CPU 更快的几乎不可见的方式(这只是一种长篇大论的无序执行方式)。

因为rdtsc它很重要,因为这意味着您可能没有为您期望的代码计时。例如,给定以下序列1

rdtsc
mov ecx, eax
mov rdi, [rdi]
mov rdi, [rdi]
rdtsc
Run Code Online (Sandbox Code Playgroud)

您可能希望rdtsc测量追逐加载负载的两个指针的延迟mov rdi, [rdi]。然而,在实践中,即使这两个加载都需要查看时间(如果它们在缓存中丢失,则为 100 秒),您将获得相当小的读取值rdtsc。问题是第二个rdtsc不等待加载完成,它只是乱序执行,所以你没有按你认为的时间间隔计时。也许这两rdtsc条指令实际上甚至在第一次加载开始之前就执行了,这取决于rdi在此示例之前的代码中是如何计算的。

到目前为止,这听起来更像是对一个没人问的问题的回答,而不是一个真正的问题,但我已经到了那里。

您有两个基本用例rdtsc

  • 作为一个快速时间戳,您通常可以不关心它如何与周围代码重新排序,因为无论如何您可能没有关于时间戳应该在哪里使用的指令级概念。
  • 作为一种精确的计时机制,例如,在微基准测试中。在这种情况下,您通常会rdtsc根据lfence说明防止重新订购。对于上面的示例,您可能会执行以下操作:

    lfence
    rdtsc
    lfence
    mov ecx, eax
    ...
    lfence
    rdtsc
    
    Run Code Online (Sandbox Code Playgroud)

    确保定时指令 ( ...) 不会逃逸到定时区域之外,并确保来自时间区域内的指令不会进入(可能问题不大,但它们可能会与您想要的代码竞争资源测量)。

多年后,英特尔看不起我们这些可怜的程序员,并提出了一条新指令:rdtscp. 就像rdtsc它返回时间戳计数器的读数一样,这家伙做了更多的事情:它使用时间戳读数原子地读取特定于内核的 MSR 值。在大多数操作系统上,这包含一个核心 ID 值。我认为这个想法是可以使用这个值在每个内核可能具有不同 TSC 偏移量的 CPU 上正确地将返回值调整为实时。

伟大的。

rdtscp引入的另一件事是在乱序执行方面的半围栏

手册

RDTSCP 指令不是序列化指令,但它会等到所有先前的指令都执行完毕并且所有先前的加载都是全局可见的。 1 但它不会等待先前的存储全局可见,并且后续指令可能会在读取之前开始执行操作被执行。

所以这就像在lfence之前rdtscp而不是在之后。这种半围栏行为的意义何在?如果你想要一个通用的时间戳并且不关心指令排序,那么不受限制的行为就是你想要的。如果您想将其用于对短代码部分进行计时,则半围栏行为仅对第二次(最终)阅读有用,但不适用于初始阅读,因为围栏位于“错误”一侧(实际上您想要两边都有围栏,但把它们放在里面可能是最重要的)。

这样的半围栏有什么作用?


1在这种情况下,我忽略了计数器的高 32 位。