rdpmc:令人惊讶的行为

7

我正在尝试理解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

(该实现是rdpmc_instructions()的翻译。)

我计算出在执行rdpmc指令之前,此代码应该执行2*ebx+3条指令,因此我期望(在这种情况下)我应该得到一个返回状态为23。

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

我做错了什么?


3
你无法获取306的返回状态,因为只有退出值的最低有效8位被返回给父进程。 - Ross Ridge
1
代码审查:for(i=0 ; i<1000; i++) 更好的翻译是将循环计数器移动到寄存器中。或者使用 cmp eax, 1000。使用 a dq 100 只会让代码变得混乱;内联小的只读常量。(如果您仍想在代码前定义,请使用 equ)。1<<30 的正确翻译是 mov ecx, 1<<30,而不是运行时移位。更有效率的循环结构是 dec ebx / jnz .looprdpmc 将 EAX 和 EDX 写入,并隐式地零扩展为 RAX 和 RDX,您不需要先将它们清零。此外,除非计数可能大于 2^32,否则可以忽略 RDX。 - Peter Cordes
此外,如果您在程序运行之前没有采取任何特殊措施来重置性能计数器,那么就没有理由指望它从零开始计数。这就是使用perf的意义所在。但是您可以采取增量方式。 - Peter Cordes
1
@PeterCordes:当我在perf下运行它时,我得到了27个指令,这是确定的,大约正确。 - user14717
@AndreasAbel:你能否编辑Peter的答案,以便我们可以在这个问题上得到权威的答案并设置该寄存器中的正确位?我认为这对大家都有普遍的兴趣。 - user14717
显示剩余10条评论
2个回答

8
固定计数器并不是一直在计数,只有在软件启用它们时才会计数。通常(内核端的)perf 会在程序开始之前将其重置为零。
与可编程计数器一样,固定计数器也有位控制它们是否在用户、内核或用户+内核(即始终)中进行计数。我猜想 Linux 的 perf 内核代码在它们没有被使用时保持它们的状态为未计数。
如果你想自己使用原始的 RDPMC,你需要通过设置 IA32_PERF_GLOBAL_CTRL 和 IA32_FIXED_CTR_CTRL MSR 中相应的位来编程/启用计数器,或者让 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.outperf stat -e instructions:u ./a.out下运行,我们总是从echo $?得到23 (instructions:u显示为30,比程序实际运行的指令数多1,包括syscall)。
23条指令恰好是第一个rdpmc之后但包括第二个rdpmc之后的严格指令数。
如果我们注释掉第一个rdpmc并在perf stat -e instructions:u下运行,则退出状态始终为26并从perf得到29rdpmc是要执行的第24条指令。(因为这是Linux静态可执行文件,所以在_start之前没有动态链接器运行,所以RAX始终初始化为零。) 我想知道内核中的sysret是否被计为"用户"指令。
但是,如果注释掉第一个rdpmc并在perf stat -e instructions (不是-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

1
请注意,rdpmc不是序列化指令。为了获得可靠的结果,它必须夹在像lfence这样的序列化指令之间。 - Andreas Abel
如果没有针对特定性能事件编程的 IA32_PERFEVTSEL 被设置为 rdpmc 操作数,会怎么样呢? @PeterCordes - Some Name
@某个名字:我真的不知道。我猜测事件计数器不会增加,你每次从rdpmc得到的输出都是相同的。可能总是0,我不知道。 - Peter Cordes
@SomeName,您是指如果PMC被禁用了,如果PMC不存在,或者如果PMC尚未编程,或者如果PMC已被禁用然后重新启用,或者如果PMC被编程为非事件,您需要我查找吗? - Lewis Kelsey
@PeterCordes 如果你正在使用 perf_event_open(2) 来使用多个可编程的 PERF_TYPE_RAW 计数器(比如 LSD_UOPSIDQ_DSB_UOPS),那么你如何确定每个可编程事件对应哪个索引值,以便为给定的 rdpmc '调用' 设置 ecx?在使用 ecx=0 打开第一个计数器和 ecx=1 打开第二个计数器时,看到一些奇怪的值。到目前为止还没有找到指南。jevents 有一些类似的东西,但它使用的是 sampling 而不是 counting - Noah
显示剩余9条评论

5

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

sudo rdmsr -a 0x38F

值为0x38F的是IA32_PERF_GLOBAL_CTRL MSR寄存器的地址,选项-a指定应在所有逻辑核心上执行rdmsr指令。默认情况下,这应该为所有逻辑核心打印出7000000ff(当HT被禁用时)或70000000f(当HT被启用时)。对于INST_RETIRED.ANY固定功能性能计数器,索引为32的位是使其可用的位,因此它应为1。值7000000ff表示所有三个固定功能计数器和所有八个可编程计数器都已启用。 IA32_PERF_GLOBAL_CTRL寄存器每个逻辑核心都有一个使能位以控制每个性能计数器。每个可编程性能计数器还有其专用控制寄存器,所有固定功能计数器也有一个控制寄存器。特别地,INST_RETIRED.ANY固定功能性能计数器的控制寄存器是IA32_FIXED_CTR_CTRL,其布局在英特尔手册第3卷18-7图中显示。寄存器中定义了12个位,前4个位可以用于控制第一个固定功能计数器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个核心上运行。


我已经按照这些指示操作了一遍,它们有效! - user14717

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接