rdpmc:令人惊讶的行为
rdpmc: surprising behavior
我正在尝试理解 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 条指令,所以我希望(在这种情况下)我应该得到一个 return 状态 23.
如果我 运行 perf stat -e instruction:u ./a.out
在这个二进制文件上,perf
告诉我我已经执行了 30 条指令,这看起来是正确的。但是,如果我执行二进制文件,我会得到一个 return 状态 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超过本程序实际指令数运行s,其中syscall
)
23 条指令正好是第一个 rdpmc
之后的指令数,但包括第二个 rdpmc
.
如果我们在 perf stat -e instructions:u
下注释掉第一个 rdpmc
和 运行,我们始终得到 26
作为退出状态,而 29
来自perf
。 rdpmc
是要执行的第24条指令。 (并且 RAX 开始初始化为零,因为这是一个 Linux 静态可执行文件,所以动态链接器在 _start
之前没有 运行)。我想知道内核中的 sysret
是否算作 "user" 指令。
但是第一个 rdpmc
被注释掉,运行ning 在 perf stat -e instructions
下(不是 :u)给出任意值,因为计数器的起始值不固定。所以我们只是将 (some arbitrary starting point + 26) mod 256 作为退出状态。
但注意 RDPMC 不是序列化指令,可以乱序执行。一般来说,您可能需要 lfence
,或者(正如 John McCalpin 在您链接的线程中所建议的那样)让 ECX 错误地依赖于您关心的指令的结果。例如and ecx, 0
/ or ecx, 1<<30
有效,因为与 xor-zeroing 不同,and ecx,0
不是 dependency-breaking.
这个程序没有什么奇怪的地方,因为front-end是唯一的瓶颈,所以所有的指令基本上都是一发出就执行。此外,rdpmc
就在循环之后,因此 loop-exit 分支的分支预测错误可能会阻止它在循环完成之前被发布到 OoO back-end。
PS 对于未来的读者:在 Linux 上启用 user-space RDPMC 的一种方法,无需任何超出 perf
要求的自定义 mod 规则记录在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寄存器中启用了您要使用的性能计数器,其布局如图18-8所示英特尔手册卷3(2019年1月) .您可以通过加载 MSR 内核模块 (sudo modprobe msr
) 并执行以下命令轻松完成此操作:
sudo rdmsr -a 0x38F
值 0x38F 是 IA32_PERF_GLOBAL_CTRL
MSR 寄存器的地址,-a
选项指定 rdmsr
指令应在所有逻辑内核上执行。默认情况下,这应该为所有逻辑内核打印 7000000ff
(当 HT 被禁用时)或 70000000f
(当 HT 被启用时)。对于 INST_RETIRED.ANY
fixed-function 性能计数器,索引 32 处的位是启用它的位,因此它应该为 1。值 7000000ff
所有三个 fixed-function计数器和所有八个可编程计数器均已启用。
IA32_PERF_GLOBAL_CTRL
寄存器针对每个逻辑内核的每个性能计数器都有一个启用位。每个可编程性能计数器也有其专用的控制寄存器,并且所有 fixed-function 计数器都有一个控制寄存器。具体来说,INST_RETIRED.ANY
fixed-function性能计数器的控制寄存器是IA32_FIXED_CTR_CTRL
,其布局如图18-7英特尔手册第3卷中所示。有12个定义位寄存器,前 4 位可用于控制第一个 fixed-function 计数器的行为,即 INST_RETIRED.ANY
(顺序如 Table 19-2 所示)。在修改寄存器之前,您应该首先检查它是如何被 OS 初始化的,方法是执行:
sudo rdmsr -a 0x38D
它应该默认打印 0xb0。这表明第二个 fixed-function 计数器(unhalted core cycles)被启用并配置为在管理员模式和用户模式下计数。要启用 INST_RETIRED.ANY
并将其配置为仅计算用户模式事件,同时保持未暂停的核心周期计数器不变,请执行以下命令:
sudo wrmsr -a 0x38D 0xb2
执行此命令后,将立即对事件进行计数。您可以通过阅读第一个 fixed-function 计数器 IA32_PERF_FIXED_CTR0
(参见 Table 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 上。
我正在尝试理解 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 条指令,所以我希望(在这种情况下)我应该得到一个 return 状态 23.
如果我 运行 perf stat -e instruction:u ./a.out
在这个二进制文件上,perf
告诉我我已经执行了 30 条指令,这看起来是正确的。但是,如果我执行二进制文件,我会得到一个 return 状态 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超过本程序实际指令数运行s,其中syscall
)
23 条指令正好是第一个 rdpmc
之后的指令数,但包括第二个 rdpmc
.
如果我们在 perf stat -e instructions:u
下注释掉第一个 rdpmc
和 运行,我们始终得到 26
作为退出状态,而 29
来自perf
。 rdpmc
是要执行的第24条指令。 (并且 RAX 开始初始化为零,因为这是一个 Linux 静态可执行文件,所以动态链接器在 _start
之前没有 运行)。我想知道内核中的 sysret
是否算作 "user" 指令。
但是第一个 rdpmc
被注释掉,运行ning 在 perf stat -e instructions
下(不是 :u)给出任意值,因为计数器的起始值不固定。所以我们只是将 (some arbitrary starting point + 26) mod 256 作为退出状态。
但注意 RDPMC 不是序列化指令,可以乱序执行。一般来说,您可能需要 lfence
,或者(正如 John McCalpin 在您链接的线程中所建议的那样)让 ECX 错误地依赖于您关心的指令的结果。例如and ecx, 0
/ or ecx, 1<<30
有效,因为与 xor-zeroing 不同,and ecx,0
不是 dependency-breaking.
这个程序没有什么奇怪的地方,因为front-end是唯一的瓶颈,所以所有的指令基本上都是一发出就执行。此外,rdpmc
就在循环之后,因此 loop-exit 分支的分支预测错误可能会阻止它在循环完成之前被发布到 OoO back-end。
PS 对于未来的读者:在 Linux 上启用 user-space RDPMC 的一种方法,无需任何超出 perf
要求的自定义 mod 规则记录在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寄存器中启用了您要使用的性能计数器,其布局如图18-8所示英特尔手册卷3(2019年1月) .您可以通过加载 MSR 内核模块 (sudo modprobe msr
) 并执行以下命令轻松完成此操作:
sudo rdmsr -a 0x38F
值 0x38F 是 IA32_PERF_GLOBAL_CTRL
MSR 寄存器的地址,-a
选项指定 rdmsr
指令应在所有逻辑内核上执行。默认情况下,这应该为所有逻辑内核打印 7000000ff
(当 HT 被禁用时)或 70000000f
(当 HT 被启用时)。对于 INST_RETIRED.ANY
fixed-function 性能计数器,索引 32 处的位是启用它的位,因此它应该为 1。值 7000000ff
所有三个 fixed-function计数器和所有八个可编程计数器均已启用。
IA32_PERF_GLOBAL_CTRL
寄存器针对每个逻辑内核的每个性能计数器都有一个启用位。每个可编程性能计数器也有其专用的控制寄存器,并且所有 fixed-function 计数器都有一个控制寄存器。具体来说,INST_RETIRED.ANY
fixed-function性能计数器的控制寄存器是IA32_FIXED_CTR_CTRL
,其布局如图18-7英特尔手册第3卷中所示。有12个定义位寄存器,前 4 位可用于控制第一个 fixed-function 计数器的行为,即 INST_RETIRED.ANY
(顺序如 Table 19-2 所示)。在修改寄存器之前,您应该首先检查它是如何被 OS 初始化的,方法是执行:
sudo rdmsr -a 0x38D
它应该默认打印 0xb0。这表明第二个 fixed-function 计数器(unhalted core cycles)被启用并配置为在管理员模式和用户模式下计数。要启用 INST_RETIRED.ANY
并将其配置为仅计算用户模式事件,同时保持未暂停的核心周期计数器不变,请执行以下命令:
sudo wrmsr -a 0x38D 0xb2
执行此命令后,将立即对事件进行计数。您可以通过阅读第一个 fixed-function 计数器 IA32_PERF_FIXED_CTR0
(参见 Table 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 上。