【问题标题】:rdpmc: surprising behaviorrdpmc:令人惊讶的行为
【发布时间】:2019-10-05 03:43:19
【问题描述】:

我正在尝试理解 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.outperf 告诉我我已经执行了 30 条指令,看起来差不多。但是如果我执行二进制文件,我会得到 58 或 0 的返回状态,不是确定性的。

我在这里做错了什么?

【问题讨论】:

  • 您无法获得 306 的返回状态,因为只有退出值的最低有效 8 位返回给父进程。
  • 您是否尝试过计算进入_start 与结束之间的增量?您是否尝试过增加迭代次数以查看结果是否随执行的指令而变化根本
  • 代码审查:for(i=0 ; i&lt;1000; i++) 的更好翻译是向循环计数器注册的 mov-immediate。或cmp eax, 1000。使用a dq 100 只是混乱;内联小的只读常量。 (如果您仍然想要代码之前的定义,请使用equ)。 1&lt;&lt;30 的正确翻译是 mov ecx, 1&lt;&lt;30,而不是运行时移位。更有效的循环结构是dec ebx / jnz .looprdpmc 写入 EAX 和 EDX,隐式零扩展到 RAX 和 RDX,您不需要先将它们归零。此外,您也可以忽略 RDX,除非计数可能 > 2^32。
  • 另外不要忘记使用default rel 所以[a] 使用RIP-relative 寻址模式。 (除非您尝试尝试 rel 和 abs 寻址模式之间的区别)。
  • @PeterCordes:当我在 perf 下运行它时,我得到 27 条指令,这是确定性的,这是正确的。

标签: performance assembly x86 performancecounter intel-pmu


【解决方案1】:

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

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

如果您想自己使用原始 RDPMC,您需要编程/启用计数器(通过设置 IA32_PERF_GLOBAL_CTRLIA32_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.outperf stat -e instructions:u ./a.out下运行这个,我们总是从echo $?得到23instructions: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)下运行会给出任意值,因为计数器的起始值不固定。所以我们只是将(任意起点 + 26)mod 256 作为退出状态。

但请注意,RDPMC 不是序列化指令,并且可以乱序执行。一般来说,您可能需要lfence,或者(正如 John McCalpin 在您链接的线程中所建议的那样)让 ECX 错误地依赖您关心的指令的结果。例如and ecx, 0 / or ecx, 1&lt;&lt;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

【讨论】:

  • 这条指令有点奇怪,因为它不会在计数器未启用时出现段错误,它只是。 . .做错事。此外,我在英特尔手册中找不到任何内容说明需要做什么才能让计数器运行。
  • 请注意,rdpmc 不是序列化指令。为了获得可靠的结果,它必须夹在序列化指令之间,例如lfence
  • @AndreasAbel 好点。该程序除了前端之外不包含任何瓶颈,因此指令都将在它们的微指令进入无序后端时尽快执行。循环退出时的分支丢失可能会有所帮助。 John McCalpin 在the thread the OP linked 上的一篇帖子包括让ECX 对您要测量的代码结果产生错误依赖的想法。 (例如and ecx,0(非破坏性)/or ecx, 1&lt;&lt;30)。
  • @PeterCordes 但这不会阻止后面的指令(在此示例中,例如 mov eax, 60)可能在 rdpmc 之前执行。
  • @AndreasAbel:是的,所以你可能仍然想要lfence after rdpmc,即使你之前使用了这个技巧来避免它。但在这种情况下,我们不必担心后面的指令:它们不能在rdpmc 执行之前retire,因为retire 是有序的。 1&lt;&lt;30 固定计数器计数 inst_retired.any,IIRC。
【解决方案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 上运行。

【讨论】:

  • 我已经阅读了这些说明,并且它们有效!
猜你喜欢
  • 1970-01-01
  • 2021-08-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-27
  • 2015-10-19
  • 2012-08-11
  • 1970-01-01
相关资源
最近更新 更多