rdpmc:令人惊讶的行为

use*_*717 7 performance x86 assembly performancecounter intel-pmu

我试图理解 rdpmc 指令。因此,我有以下汇编代码:

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
Run Code Online (Sandbox Code Playgroud)

(实现是rdpmc_instructions()的翻译。)我认为这段代码应该在命中指令之前执行 2*ebx+3rdpmc条指令,所以我期望(在这种情况下)我应该得到 23 的返回状态。

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

我在这里做错了什么?

Pet*_*des 7

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

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

如果您想自己使用原始 RDPMC,您需要编程/启用计数器(通过设置IA32_PERF_GLOBAL_CTRLIA32_FIXED_CTR_CTRLMSR 中的相应位),或者通过在perf. 例如perf stat ./a.out

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

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

这也意味着构建完整的 64 位rdpmc结果毫无意义:输入的高 32 位不会影响结果的低 8 位,sub因为进位仅从低到高传播。通常,除非您期望计数 > 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.
Run Code Online (Sandbox Code Playgroud)

perf stat ./a.outor下运行这个perf stat -e instructions:u ./a.out,我们总是23echo $? (instructions:u显示 30,比这个程序运行的实际指令数多 1,包括syscall)

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

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

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

但要注意 RDPMC不是序列化指令,可以乱序执行。一般来说,您可能需要lfence,或者(正如 John McCalpin 在您链接的线程中所建议的那样)给予 ECX 对您关心的指令结果的错误依赖。例如and ecx, 0/or ecx, 1<<30有效,因为与异或归零不同,and ecx,0它不会破坏依赖性。

这个程序没有什么奇怪的事情发生,因为前端是唯一的瓶颈,所以所有的指令基本上都是一发出就执行的。此外,rdpmc循环紧随其后,因此循环退出分支的分支预测错误可能会阻止它在循环完成之前被发送到 OoO 后端。


面向未来读者的 PS:一种在 Linux 上启用用户空间 RDPMC 且无需任何超出perf要求的自定义模块的方法记录在perf_event_open(2)

echo 2 | sudo tee /sys/devices/cpu/rdpmc    # enable RDPMC always, not just when a perf event is open
Run Code Online (Sandbox Code Playgroud)


Had*_*ais 5

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

sudo rdmsr -a 0x38F
Run Code Online (Sandbox Code Playgroud)

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

IA32_PERF_GLOBAL_CTRL寄存器对于每个逻辑核心的每个性能计数器都有一个使能位。每个可编程性能计数器还有其专用控制寄存器,并且所有固定功能计数器都有一个控制寄存器。特别是,INST_RETIRED.ANY固定功能性能计数器的控制寄存器为IA32_FIXED_CTR_CTRL,其布局如 Intel 手册第 3 卷的图 18-7 所示。寄存器中有 12 个定义位,前 4 位可用于控制第一个固定功能计数器的行为,即INST_RETIRED.ANY(顺序如表 19-2 所示)。在修改寄存器之前,您应该首先检查操作系统如何初始化它,方法是执行:

sudo rdmsr -a 0x38D
Run Code Online (Sandbox Code Playgroud)

默认情况下,它应该打印 0xb0。这表示第二个固定功能计数器(未停止的核心周期)已启用并配置为在管理员模式和用户模式下进行计数。要启用INST_RETIRED.ANY并将其配置为仅对用户模式事件进行计数,同时保持未停止的核心周期计数器不变,请执行以下命令:

sudo wrmsr -a 0x38D 0xb2
Run Code Online (Sandbox Code Playgroud)

一旦执行该命令,立即对事件进行计数。您可以通过读取第一个固定功能计数器来检查这一点IA32_PERF_FIXED_CTR0(参见表 19-2):

sudo rdmsr -a 0x309
Run Code Online (Sandbox Code Playgroud)

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

sudo wrmsr -a 0x309 0
Run Code Online (Sandbox Code Playgroud)

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

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

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