rdpmc:令人惊讶的行为

rdpmc: surprising behavior

我正在尝试理解 rdpmc 指令。因此,我有以下 asm 代码:​​

segment .text
global _start

_start:
    xor eax, eax
    mov ebx, 10
.loop:
    dec ebx
    jnz .loop

    mov ecx, 1<<30
    ; calling rdpmc with ecx = (1<<30) gives number of retired instructions
    rdpmc
    ; but only if you do a bizarre incantation: (Why u do dis Intel?)
    shl rdx, 32
    or  rax, rdx

    mov rdi, rax ; return number of instructions retired.
    mov eax, 60
    syscall

(实现是rdpmc_instructions()的翻译。) 我计算这段代码应该在命中 rdpmc 指令之前执行 2*ebx+3 条指令,所以我希望(在这种情况下)我应该得到一个 return 状态 23.

如果我 运行 perf stat -e instruction:u ./a.out 在这个二进制文件上,perf 告诉我我已经执行了 30 条指令,这看起来是正确的。但是,如果我执行二进制文件,我会得到一个 return 状态 58,或 0,不确定。

我做错了什么?

固定计数器不会一直计数,只有在软件启用它们时才计数。通常(内核端)perf 会这样做,并在启动程序之前将它们重置为零。

固定计数器(如可编程计数器)具有控制是否 它们计入用户、内核或用户+内核(即始终)。我假设 Linux 的 perf 内核代码将它们设置为在没有使用它们时都不计数。

如果您想自己使用原始 RDPMC,您需要编程/启用计数器(通过设置 IA32_PERF_GLOBAL_CTRLIA32_FIXED_CTR_CTRL MSR 中的相应位),或者让 perf 执行仍然 运行 在 perf 下为你设置程序。例如perf stat ./a.out

如果您使用 perf stat -e instructions:u ./perf ; echo $?,固定计数器实际上会在输入您的代码之前清零,因此您使用一次 rdpmc 可以获得一致的结果。否则,例如使用默认值 -e instructions(不是 :u),您不知道计数器的初始值。您可以通过获取增量来解决此问题,在开始时读取计数器一次,然后在循环后读取一次。

退出状态只有 8 位宽,所以这个避免 printf 或 write() 的小技巧只适用于非常小的计数。

这也意味着构建完整的 64 位 rdpmc 结果毫无意义:输入的高 32 位不会影响 sub 结果的低 8 位,因为进位传播只能从低到高。通常,除非您期望计数 > 2^32,否则只需使用 EAX 结果。即使原始 64 位计数器在您测量的时间间隔内回绕,您的减法结果仍然是 32 位寄存器中的正确小整数。


比你的问题更简单。还要注意缩进操作数,这样即使对于超过 3 个字母的助记符,它们也可以保持一致。

segment .text
global _start

_start:
    mov   ecx, 1<<30      ; fixed counter: instructions
    rdpmc
    mov   edi, eax        ; start

    mov   edx, 10
.loop:
    dec   edx
    jnz   .loop

    rdpmc               ; ecx = same counter as before

    sub   eax, edi       ; end - start

    mov   edi, eax
    mov   eax, 231
    syscall             ; sys_exit_group(rdpmc).  sys_exit isn't wrong, but glibc uses exit_group.

运行这个在perf stat ./a.outperf stat -e instructions:u ./a.out下,我们总是从echo $?得到23instructions:u显示30,也就是1超过本程序实际指令数运行s,其中syscall)

23 条指令正好是第一个 rdpmc 之后的指令数,但包括第二个 rdpmc.

如果我们在 perf stat -e instructions:u 下注释掉第一个 rdpmc 和 运行,我们始终得到 26 作为退出状态,而 29 来自perfrdpmc是要执行的第24条指令。 (并且 RAX 开始初始化为零,因为这是一个 Linux 静态可执行文件,所以动态链接器在 _start 之前没有 运行)。我想知道内核中的 sysret 是否算作 "user" 指令。

但是第一个 rdpmc 被注释掉,运行ning 在 perf stat -e instructions 下(不是 :u)给出任意值,因为计数器的起始值不固定。所以我们只是将 (some arbitrary starting point + 26) mod 256 作为退出状态。

但注意 RDPMC 不是序列化指令,可以乱序执行。一般来说,您可能需要 lfence,或者(正如 John McCalpin 在您链接的线程中所建议的那样)让 ECX 错误地依赖于您关心的指令的结果。例如and ecx, 0 / or ecx, 1<<30 有效,因为与 xor-zeroing 不同,and ecx,0 不是 dependency-breaking.

这个程序没有什么奇怪的地方,因为front-end是唯一的瓶颈,所以所有的指令基本上都是一发出就执行。此外,rdpmc 就在循环之后,因此 loop-exit 分支的分支预测错误可能会阻止它在循环完成之前被发布到 OoO back-end。


PS 对于未来的读者:在 Linux 上启用 user-space RDPMC 的一种方法,无需任何超出 perf 要求的自定义 mod 规则记录在perf_event_open(2):

echo 2 | sudo tee /sys/devices/cpu/rdpmc    # enable RDPMC always, not just when a perf event is open

第一步是确保在IA32_PERF_GLOBAL_CTRL MSR寄存器中启用了您要使用的性能计数器,其布局如图18-8所示英特尔手册卷3(2019年1月) .您可以通过加载 MSR 内核模块 (sudo modprobe msr) 并执行以下命令轻松完成此操作:

sudo rdmsr -a 0x38F

值 0x38F 是 IA32_PERF_GLOBAL_CTRL MSR 寄存器的地址,-a 选项指定 rdmsr 指令应在所有逻辑内核上执行。默认情况下,这应该为所有逻辑内核打印 7000000ff(当 HT 被禁用时)或 70000000f(当 HT 被启用时)。对于 INST_RETIRED.ANY fixed-function 性能计数器,索引 32 处的位是启用它的位,因此它应该为 1。值 7000000ff 所有三个 fixed-function计数器和所有八个可编程计数器均已启用。

IA32_PERF_GLOBAL_CTRL 寄存器针对每个逻辑内核的每个性能计数器都有一个启用位。每个可编程性能计数器也有其专用的控制寄存器,并且所有 fixed-function 计数器都有一个控制寄存器。具体来说,INST_RETIRED.ANYfixed-function性能计数器的控制寄存器是IA32_FIXED_CTR_CTRL,其布局如图18-7英特尔手册第3卷中所示。有12个定义位寄存器,前 4 位可用于控制第一个 fixed-function 计数器的行为,即 INST_RETIRED.ANY(顺序如 Table 19-2 所示)。在修改寄存器之前,您应该首先检查它是如何被 OS 初始化的,方法是执行:

sudo rdmsr -a 0x38D

它应该默认打印 0xb0。这表明第二个 fixed-function 计数器(unhalted core cycles)被启用并配置为在管理员模式和用户模式下计数。要启用 INST_RETIRED.ANY 并将其配置为仅计算用户模式事件,同时保持未暂停的核心周期计数器不变,请执行以下命令:

sudo wrmsr -a 0x38D 0xb2

执行此命令后,将立即对事件进行计数。您可以通过阅读第一个 fixed-function 计数器 IA32_PERF_FIXED_CTR0(参见 Table 19-2)来检查这一点:

sudo rdmsr -a 0x309

您可以多次执行该命令并查看每个核心上的计数如何变化。不幸的是,这意味着当你的程序是 运行 时,IA32_PERF_FIXED_CTR0 中的当前值基本上是一些随机值。您可以尝试通过执行以下命令来重置计数器:

sudo wrmsr -a 0x309 0

但根本问题依然存在;您无法立即重置计数器和 运行 您的程序。正如@Peter 的回答中所建议的那样,使用任何性能计数器的正确方法是将感兴趣的区域包装在 rdpmc 指令之间并取差值。

MSR 内核模块非常方便,因为访问 MSR 寄存器的唯一方法是在内核模式下。但是,有一种替代方法可以将代码包装在 rdpmc 指令之间。您可以编写自己的内核模块,并在启用计数器的指令之后立即将您的代码放入内核模块中。您甚至可以禁用中断。通常,这种准确性水平不值得付出努力。

您可以使用 -p 选项而不是 -a 来指定特定的逻辑核心。但是,您必须确保程序 运行 在同一个内核上,例如 taskset -c 3 ./a.out 到 运行 在内核 #3 上。