7

I'm trying to understand the rdpmc instruction. As such I have the following asm code:

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

(The implementation is a translation of rdpmc_instructions().) I count that this code should execute 2*ebx+3 instructions before hitting the rdpmc instruction, so I expect (in this case) that I should get a return status of 23.

If I run perf stat -e instruction:u ./a.out on this binary, perf tells me that I've executed 30 instructions, which looks about right. But if I execute the binary, I get a return status of 58, or 0, not deterministic.

What have I done wrong here?

4

2 回答 2

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.

perf stat ./a.out在or下运行这个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
于 2019-05-17T21:36:35.913 回答
5

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

sudo rdmsr -a 0x38F

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

IA32_PERF_GLOBAL_CTRL寄存器对于每个逻辑内核的每个性能计数器都有一个启用位。每个可编程性能计数器也有其专用的控制寄存器,并且所有固定功能计数器都有一个控制寄存器。具体来说,INST_RETIRED.ANY固定功能性能计数器的控制寄存器IA32_FIXED_CTR_CTRL是第一个固定功能计数器的行为,即,INST_RETIRED.ANY(顺序如表19-2所示)。在修改寄存器之前,您应该首先通过执行以下命令检查它是如何被操作系统初始化的:

sudo rdmsr -a 0x38D

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

sudo wrmsr -a 0x38D 0xb2

执行此命令后,立即对事件进行计数。您可以通过读取第一个固定功能计数器来检查这一点IA32_PERF_FIXED_CTR0(参见表 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 相同的核心上。

于 2019-05-18T00:30:26.160 回答