我试图理解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,不是确定性的。
我在这里做错了什么?
固定计数器不会一直计数,只有在软件启用它们时才计数。 通常(内核端)perf
这样做,并在启动程序之前将它们重置为零。
固定计数器(如可编程计数器)具有控制是否 它们计入用户、内核或用户+内核(即始终)。 我假设 Linux 的perf
内核代码将它们设置为在没有任何使用它们时都不计数。
如果您想自己使用原始 RDPMC,您需要编程/启用计数器(通过在IA32_PERF_GLOBAL_CTRL
和IA32_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.out
或perf stat -e instructions:u ./a.out
下运行它,我们总是从echo $?
得到23
(instructions:u
显示 30,比该程序运行的实际指令数多 1 条,包括syscall
条)
23条指令正好是严格在第一rdpmc
之后的指令数,但包括第2条rdpmc
。
如果我们注释掉第一个rdpmc
并在perf stat -e instructions:u
下运行它,我们始终得到26
作为退出状态,并从perf
29
。rdpmc
是要执行的第 24 条指令。 (RAX 开始时初始化为零,因为这是一个 Linux 静态可执行文件,因此动态链接器在_start
之前没有运行)。 我想知道内核中的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
第一步是确保在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 上运行。