rdpmc:令人惊讶的行为

时间:2019-05-17 19:43:56

标签: performance assembly x86 performancecounter intel-pmu

我正在尝试了解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条指令,因此我希望(在这种情况下)返回状态为23。

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

我在这里做错了什么?

2 个答案:

答案 0 :(得分:3)

固定计数器不会一直计数,仅当软件启用它们时才会计数。通常,perf(位于内核一侧)会执行此操作,并在启动程序之前将它们重置为零。

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

如果您想自己使用原始RDPMC,则需要编程/启用计数器(通过在IA32_PERF_GLOBAL_CTRLIA32_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位不会影响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下运行它,我们总是从23得到echo $?instructions:u显示30,比实际数量多1。该程序运行的说明,包括syscall

23条指令恰好是第一个rdpmc之后但紧随第二个rdpmc之后的指令数。

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

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

但是请注意,RDMPC不是 序列化指令,并且可以无序执行。通常,您可能需要lfence,或者(如John McCalpin在所链接的线程中建议的那样)使ECX对您关心的指令结果有虚假的依赖。例如and ecx, 0 / or ecx, 1<<30之所以有效,是因为与异或归零不同,and ecx,0不会打破依赖关系。

该程序没有任何奇怪的事情发生,因为前端是唯一的瓶颈,因此所有指令基本上在它们发出后立即执行。另外,rdpmc就在循环之后,因此循环退出分支的分支预测错误可能会阻止它在循环结束之前发布到OoO后端。


面向未来读者的PS:perf_event_open(2)中记录了一种在Linux上启用用户空间RDPMC的方法,而没有perf所需的任何自定义模块:

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

答案 1 :(得分:2)

第一步是确保在IA32_PERF_GLOBAL_CTRL MSR寄存器中启用了要使用的性能计数器,其布局如《英特尔手册》第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号内核上运行。