x86的MOV真的可以"free"吗?为什么我根本不能重现这个?

Can x86's MOV really be "free"? Why can't I reproduce this at all?

我经常看到有人声称 MOV 指令在 x86 中可以自由使用,因为寄存器重命名。

对于我来说,我无法在单个测试用例中验证这一点。我尝试的每个测试用例都会揭穿它。

例如,这是我用 Visual C++ 编译的代码:

#include <limits.h>
#include <stdio.h>
#include <time.h>

int main(void)
{
    unsigned int k, l, j;
    clock_t tstart = clock();
    for (k = 0, j = 0, l = 0; j < UINT_MAX; ++j)
    {
        ++k;
        k = j;     // <-- comment out this line to remove the MOV instruction
        l += j;
    }
    fprintf(stderr, "%d ms\n", (int)((clock() - tstart) * 1000 / CLOCKS_PER_SEC));
    fflush(stderr);
    return (int)(k + j + l);
}

这将为循环生成以下汇编代码(您可以随意生成它;您显然不需要 Visual C++):

LOOP:
    add edi,esi
    mov ebx,esi
    inc esi
    cmp esi,FFFFFFFFh
    jc  LOOP

现在我 运行 这个程序好几次了,当删除 MOV 指令时,我观察到一个非常一致的 2% 的差异:

Without MOV      With MOV
  1303 ms         1358 ms
  1324 ms         1363 ms
  1310 ms         1345 ms
  1304 ms         1343 ms
  1309 ms         1334 ms
  1312 ms         1336 ms
  1320 ms         1311 ms
  1302 ms         1350 ms
  1319 ms         1339 ms
  1324 ms         1338 ms

那么是什么原因呢?为什么不是 MOV "free"?这个循环对于 x86 来说太复杂了吗?
是否有单个示例可以证明 MOV 像人们声称的那样是免费的?
如果是这样,它是什么?如果不是,为什么每个人都声称 MOV 是免费的?

这里有两个小测试,我认为它们最终显示了移动消除的证据:

__loop1:
    add edx, 1
    add edx, 1
    add ecx, 1
    jnc __loop1

对比

__loop2:
    mov eax, edx
    add eax, 1
    mov edx, eax
    add edx, 1
    add ecx, 1
    jnc __loop2

如果mov 向依赖链添加一个循环,预计第二个版本每次迭代大约需要 4 个循环。在我的 Haswell 上,每次迭代都需要大约 2 个周期,如果没有移动消除,这是不可能发生的。

Register-copy 对于 front-end 永远不会免费,只是在 issue/rename 阶段从 back-end 中实际执行(零延迟)中删除 CPUs:

  • 用于 XMM 向量寄存器的 AMD Bulldozer 系列,不是整数。
  • 用于整数和 XMM 向量寄存器的 AMD Zen 系列。 (以及 Zen2 及更高版本中的 YMM)
    (有关 BD / Zen 1 中 YMM 的 low/high 一半的详细信息,请参阅 Agner Fog's 微架构指南)
  • 用于整数和向量寄存器的 Intel Ivy Bridge 及更高版本(MMX 除外)
  • Intel Goldmont 及更高版本 low-power CPUs:XMM 和整数
    (包括 Alder Lake E-cores integer/XMM 但不包括 YMM)
  • 不是 Intel Ice Lake:禁用微代码更新 register-renaming 作为解决错误的一部分,用于 general-purpose 整数 reg。 XMM/YMM/ZMM 重命名仍然有效。 Tiger Lake 也受到影响,但 Rocket Lake 或 Alder Lake 没有受到影响 P-cores.
    uops.info results for mov r32, r32 - 注意 CPUs 的延迟为 0。请注意,它们在 Ice Lake 和 Tiger Lake 上显示 latency=1,因此它们在微代码更新后重新测试。

你的实验

问题中循环的吞吐量不依赖于 MOV 的延迟,或者(在 Haswell 上)不使用执行的好处单位.

front-end 的循环仍然只有 4 微指令发送到 out-of-order back-end。 (mov 仍然需要被 out-of-order back-end 跟踪,即使它不需要执行单元,但是 cmp/jc macro-fuses 变成一个 uop) .

Intel CPUs 因为 Core 2 的问题宽度为每时钟 4 微指令,所以 mov 不会阻止它在(接近)每个时钟上执行一个迭代哈斯韦尔。在 Ivybridge 上它也会 运行 每个时钟一个(mov-elimination),但在 Sandybridge 上 不会 (没有 mov-elimination)。 在 SnB 上,每 1.333c 周期大约有一个迭代器,这在 ALU 吞吐量上是瓶颈,因为 mov 总是需要一个 。 (SnB/IvB 只有三个 ALU 端口,而 Haswell 有四个)。

请注意,重命名阶段的特殊处理对于 x87 FXCHG(将 st0st1 交换)的时间比 MOV 长得多。 Agner Fog 在 PPro/PII/PIII(first-gen P6 核心)上将 FXCHG 列为 0 延迟。


题中的循环有两个互锁的依赖链(add edi,esi依赖于EDI和循环计数器ESI),这使得它对不完美的调度更加敏感。由于 seemingly-unrelated 指令,与理论预测相比减速 2% 并不罕见,指令顺序的微小变化会造成这种差异。要 运行 每个迭代正好 1c,每个循环都需要 运行 一个 INC 和一个 ADD。由于所有 INC 和 ADD 都依赖于先前的迭代,因此 out-of-order 执行无法在一个周期内赶上 运行ning 两个。更糟糕的是,ADD 依赖于上一个循环中的 INC,这就是我所说的“互锁”的意思,因此在 INC dep 链中丢失一个循环也会使 ADD dep 链停止。

此外,predicted-taken b运行ches 只能在端口 6 上 运行,因此 任何端口 6 不执行 cmp/jc 的周期是吞吐量损失周期。每当 INC 或 ADD 在端口 6 上窃取一个周期而不是在端口 0、1 或 5 上窃取 运行ning 时,就会发生这种情况。IDK 如果这是罪魁祸首,或者如果在 INC/ADD dep 链中丢失周期他们自己是问题所在,或者两者兼而有之。

添加额外的 MOV 不会增加任何 execution-port 压力,假设它被 100% 消除,但它确实阻止了 运行 前进的 front-end back-end 个执行单位。 (循环中的 4 个微指令中只有 3 个需要一个执行单元,而您的 Haswell CPU 可以 运行 在其 4 个 ALU 端口中的任何一个上进行 INC 和 ADD:0、1、5 和 6。所以瓶颈是:

  • front-end 每个时钟 4 微指令的最大吞吐量。 (没有 MOV 的循环只有 3 微码,所以 front-end 可以 运行 提前)。
  • taken-branch 每个时钟一个吞吐量。
  • 涉及 esi 的依赖链(每个时钟 1 个 INC 延迟)
  • 涉及edi的依赖链(每个时钟增加1个延迟,并且还依赖于上一次迭代的INC)

没有 MOV,front-end 可以每个时钟 4 次发出循环的三个微指令,直到 out-of-order back-end 满为止。 (AFAICT, lsd.cycles_4_uops 的性能计数器确认它在发出任何 uops 时主要以 4 个为一组发出。)

。该决定基于跟踪调度程序(又名保留站,RS)中每个端口的微指令数的计数器。当 RS 中有很多 uops 等待执行时,这很有效并且通常应该避免将 INC 或 ADD 调度到端口 6。而且我想也避免了安排 INC 和 ADD,这样时间就会从这些 dep 链中的任何一个中丢失。但是,如果 RS 为空或 nearly-empty,计数器将不会阻止 ADD 或 INC 在端口 6 上窃取一个周期。

我以为我在做某事g 在这里,但是任何 sub-optimal 调度应该让 front-end 赶上并保持 back-end 满。我不认为我们应该期望 front-end 会在管道中引起足够多的气泡来解释低于最大吞吐量 2% 的下降,因为微小循环应该 运行 从循环缓冲区以非常一致的 4每时钟吞吐量。也许还有其他事情正在发生。


mov 消除的好处的真实例子。

我使用 lea 构建了一个每个时钟只有一个 mov 的循环,创建了一个完美的演示,其中 MOV-elimination 100% 成功,或者 0% 的时间 mov same,same 来演示产生的延迟瓶颈。

由于macro-fuseddec/jnz是涉及循环计数器的依赖链的部分,不完善的调度无法延迟. 这不同于 cmp/jc 每次迭代都从 critical-path 依赖链中“分叉”的情况。

_start:
    mov     ecx, 2000000000 ; each iteration decrements by 2, so this is 1G iters
align 16  ; really align 32 makes more sense in case the uop-cache comes into play, but alignment is actually irrelevant for loops that fit in the loop buffer.
.loop:
    mov eax, ecx
    lea ecx, [rax-1]    ; we vary these two instructions

    dec ecx             ; dec/jnz macro-fuses into one uop in the decoders, on Intel
    jnz .loop

.end:
    xor edi,edi    ; edi=0
    mov eax,231    ; __NR_exit_group from /usr/include/asm/unistd_64.h
    syscall        ; sys_exit_group(0)

在 Intel SnB-family 上,LEA 在寻址模式下具有一个或两个组件 运行s,延迟为 1c(参见 http://agner.org/optimize/, and other links in the 标签 wiki)。

我在 Linux 上构建并 运行 这是一个静态二进制文件,所以 user-space perf-counters 整个过程只测量循环,启动/关闭可忽略不计高架。 (与将 perf-counter 查询放入程序本身相比,perf stat 真的很容易)

$ yasm -felf64 -Worphan-labels -gdwarf2 mov-elimination.asm && ld -o mov-elimination mov-elimination.o &&
  objdump -Mintel -drwC mov-elimination &&
  taskset -c 1 ocperf.py stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,uops_issued.any,uops_executed.thread  -r2 ./mov-elimination

Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:       b9 00 94 35 77          mov    ecx,0x77359400
  4000b5:       66 66 2e 0f 1f 84 00 00 00 00 00        data16 nop WORD PTR cs:[rax+rax*1+0x0]

00000000004000c0 <_start.loop>:
  4000c0:       89 c8                   mov    eax,ecx
  4000c2:       8d 48 ff                lea    ecx,[rax-0x1]
  4000c5:       ff c9                   dec    ecx
  4000c7:       75 f7                   jne    4000c0 <_start.loop>

00000000004000c9 <_start.end>:
  4000c9:       31 ff                   xor    edi,edi
  4000cb:       b8 e7 00 00 00          mov    eax,0xe7
  4000d0:       0f 05                   syscall 

perf stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r2 ./mov-elimination

 Performance counter stats for './mov-elimination' (2 runs):

    513.242841      task-clock:u (msec)       #    1.000 CPUs utilized    ( +-  0.05% )
             0      context-switches:u        #    0.000 K/sec                  
             1      page-faults:u             #    0.002 K/sec                  
 2,000,111,934      cycles:u                  #    3.897 GHz              ( +-  0.00% )
 4,000,000,161      instructions:u            #    2.00  insn per cycle   ( +-  0.00% )
 1,000,000,157      branches:u                # 1948.396 M/sec            ( +-  0.00% )
 3,000,058,589      uops_issued_any:u         # 5845.300 M/sec            ( +-  0.00% )
 2,000,037,900      uops_executed_thread:u    # 3896.865 M/sec            ( +-  0.00% )

   0.513402352 seconds time elapsed                                          ( +-  0.05% )

正如预期的那样,循环 运行s 1G 次(branches ~= 10 亿次)。超过 2G 的“额外”111k 周期是其他测试中也存在的开销,包括没有 mov 的测试。它不是来自 mov-elimination 的偶尔失败,但它确实随着迭代计数而扩展,因此它不仅仅是启动开销。这可能来自定时器中断,因为 IIRC Linux perf 在处理中断时不会乱用 perf-counters,只是让它们继续计数。 (perf 虚拟化硬件性能计数器,因此即使线程在 CPU 之间迁移,您也可以获得 per-process 计数。)此外,计时器中断共享相同物理内核的同级逻辑内核会有点扰乱事情。

瓶颈是涉及循环计数器的 loop-carried 依赖链。 1G 迭代器的 2G 周期是每次迭代 2 个时钟,或每次递减 1 个时钟。这证实了 dep 链的长度是 2 个周期。 这只有在 mov 具有零延迟 时才有可能。 (我知道这并不能证明没有其他瓶颈。它实际上只能证明延迟是最多 2个周期,如果您不相信我关于延迟是唯一瓶颈的断言。有一个 resource_stalls.any 性能计数器,但它没有很多选项来分解哪个微体系结构资源已耗尽。)

循环有 3 个 fused-domain 微指令:movlea. The 3G uops_issued.any count confirms that: It counts in the fused domain, which is all of the pipeline from decoders to retirement, except for the scheduler (RS) and execution units. (macro-fused instruction-pairs stay as single uop everywhere. It's only for micro-fusion of stores or ALU+load that 1 fused-domain uop in the ROB 跟踪两个未fused-domain 微指令的进度。)

2G uops_executed.thread (unfused-domain) 告诉我们所有的 mov 微指令都被淘汰了(即由 issue/rename 阶段处理,并放在 ROB 中already-executed 状态)。它们仍然占用 issue/retire 带宽,在 uop 缓存中占用 space,以及 code-size。它们在 ROB 中占用 space,限制了 out-of-order window 大小。 A mov 指令永远不会免费。除了延迟和执行端口之外,还有许多可能的微架构瓶颈,最重要的通常是 front-end.

的 4-wide 问题率

在 Intel CPUs 上,零延迟通常比不需要执行单元更重要,尤其是在有 4 个 ALU 端口的 Haswell 和更高版本中。 (但其中只有 3 个可以处理向量微指令,所以 non-eliminated 向量移动更容易成为瓶颈,特别是在没有很多负载或存储占用 front-end 带宽的代码中(每个 4 fused-domain 微指令时钟)远离 ALU uops。此外,将 uops 调度到执行单元并不完美(首先更像是 oldest-ready),因此不在关键路径上的 uops 可以从关键路径窃取周期。)

如果我们将 nopxor edx,edx 放入循环中,它们也会发出但不会在 Intel SnB-family CPUs 上执行。

Zero-latency mov-elimination 可用于 zero-extending 从 32 位到 64 位,以及 8 位到 64 位。().


没有mov-elimination

目前所有支持mov-elimination的CPU不支持mov same,same,所以为[=367选择不同的寄存器=] 从 32 到 64 位的整数,或者 vmovdqa xmm,xmm 到 zero-extend 到 YMM 在极少数情况下是必要的。 (除非你 需要 寄存器中的结果已经存在。跳到不同的寄存器并返回通常更糟。)在 Intel 上,这同样适用于 movzx eax,al 例如. (AMD Ryzen 没有 mov-eliminate movzx。)Agner Fog 的指令 tables 显示 mov 作为 always 在 Ryzen 上被淘汰,但我猜他意味着它不会像在 Intel 上那样在两个不同的 regs 之间失败。

我们可以利用这个限制来创造一个有意击败它的micro-benchmark。

mov ecx, ecx      # CPUs can't eliminate  mov same,same
lea ecx, [rcx-1]

dec ecx
jnz .loop

 3,000,320,972      cycles:u                  #    3.898 GHz                      ( +-  0.00% )
 4,000,000,238      instructions:u            #    1.33  insn per cycle           ( +-  0.00% )
 1,000,000,234      branches:u                # 1299.225 M/sec                    ( +-  0.00% )
 3,000,084,446      uops_issued_any:u         # 3897.783 M/sec                    ( +-  0.00% )
 3,000,058,661      uops_executed_thread:u    # 3897.750 M/sec                    ( +-  0.00% )

这次拍1G迭代需要3G循环,因为现在依赖链的长度是3个循环。

fused-domain uop 计数没有改变,仍然是 3G。

改变的是现在 unfused-domain uop 计数与 fused-domain 相同。所有的 uops 都需要一个执行单元; none 的 mov 指令被删除,因此它们都向 loop-carried dep 链添加了 1c 延迟。

(当有 micro-fused 微指令时,例如 add eax, [rsi]uops_executed 计数可以 uops_issued 更高 。但我们没有。)


根本没有 mov

lea ecx, [rcx-1]

dec ecx
jnz .loop


 2,000,131,323      cycles:u                  #    3.896 GHz                      ( +-  0.00% )
 3,000,000,161      instructions:u            #    1.50  insn per cycle         
 1,000,000,157      branches:u                # 1947.876 M/sec                  
 2,000,055,428      uops_issued_any:u         # 3895.859 M/sec                    ( +-  0.00% )
 2,000,039,061      uops_executed_thread:u    # 3895.828 M/sec                    ( +-  0.00% )

现在我们将 loop-carried 深度链的延迟降低到 2 个周期。

什么都没有消除。


我在 3.9GHz i7-6700k Skylake 上进行了测试。对于所有 perf 事件,我在 Haswell i5-4210U 上得到相同的结果(在 1G 计数的 40k 以内)。这与同一系统上的 re-running 的误差幅度大致相同。

请注意,如果我 运行 perf 作为 root1,并且计算 cycles 而不是 cycles:u (user-space only),它测量的 CPU 频率正好是 3.900 GHz。 (IDK 为什么 Linux 仅在重新启动后立即服从 bios-settings 的最大涡轮增压,但如果我将其闲置几分钟则降至 3.9GHz。Asus Z170 Pro Gaming mobo,Arch Linux 与内核 4.10.11-1-ARCH。看到与 Ubuntu 相同的事情。从 /etc/rc.local 向每个 /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference 写入 balance_performance 修复它,但是写入 balance_power 使其稍后再次降回 3.9GHz。)

1:更新:作为 运行ning sudo perf 的更好替代方案,我在 /etc/syctl.d/99-local.conf

中设置了 sysctl kernel.perf_event_paranoid = 0

你应该在 AMD Ryzen 上得到相同的结果,因为它可以消除整数 mov。 AMD Bulldozer-family 只能消除 xmm 寄存器副本。 (根据 Agner Fog 的说法,ymm 寄存器副本是消除的 low-half 和高半部分的 ALU 运算。)

例如,AMD Bulldozer 和 Intel Ivybridge 可以维持每个时钟 1 个的吞吐量

 movaps  xmm0, xmm1
 movaps  xmm2, xmm3
 movaps  xmm4, xmm5
 dec
 jnz .loop

但英特尔 Sandybridge 无法消除移动,因此它会在 3 个执行端口的 4 个 ALU 微处理器上出现瓶颈。如果它是 pxor xmm0,xmm0 而不是 movaps,SnB 也可以维持每个时钟的一次迭代。 (但是 Bulldozer-family 不能,因为 xor-zeroing 在 AMD 上仍然需要一个执行单元,即使它独立于寄存器的旧值。而且 Bulldozer-family 对于 PXOR 只有 0.5c 吞吐量.)


mov-elimination

的限制

连续两条相关的 MOV 指令暴露了 Haswell 和 Skylake 之间的差异。

.loop:
  mov eax, ecx
  mov ecx, eax

  sub ecx, 2
  jnz .loop

Haswell:较小的 run-to-run 可变性(1.746 至 1.749 c / iter),但这是典型的:

 1,749,102,925      cycles:u                  #    2.690 GHz                    
 4,000,000,212      instructions:u            #    2.29  insn per cycle         
 1,000,000,208      branches:u                # 1538.062 M/sec                  
 3,000,079,561      uops_issued_any:u         # 4614.308 M/sec                  
 1,746,698,502      uops_executed_core:u      # 2686.531 M/sec                  
   745,676,067      lsd_cycles_4_uops:u       # 1146.896 M/sec                  
  

并非所有 MOV 指令都被消除:每次迭代的 2 条指令中约有 0.75 条使用了执行端口。每个执行而不是被消除的 MOV 都会给 loop-carried dep 链增加 1c 的延迟,所以 uops_executedcycles 非常相似并不是巧合。所有微指令都是单个依赖链的一部分,因此不可能存在并行性。 cycles 总是比 uops_executed 高 5M,无论 run-to-run 变化如何,所以我猜其他地方只用了 5M 周期。

Skylake:比 HSW 结果更多 table,而且更多 mov-elimination:每 2 个中只有 0.6666 个 MOV 需要一个执行单元。

 1,666,716,605      cycles:u                  #    3.897 GHz
 4,000,000,136      instructions:u            #    2.40  insn per cycle
 1,000,000,132      branches:u                # 2338.050 M/sec
 3,000,059,008      uops_issued_any:u         # 7014.288 M/sec
 1,666,548,206      uops_executed_thread:u    # 3896.473 M/sec
   666,683,358      lsd_cycles_4_uops:u       # 1558.739 M/sec

在 Haswell 上,lsd.cycles_4_uops 占了所有微指令。 (0.745 * 4 ~= 3)。因此,几乎在发出任何 uops 的每个周期中,都会发出一个完整的 4 组(来自 loop-buffer。我可能应该查看一个不关心它们来自哪里的不同计数器,比如 uops_issued.stall_cycles 计算没有发出 uops 的周期)。

但在 SKL 上,0.66666 * 4 = 2.66664 小于 3,因此在某些周期中 front-end 发出的指令少于 4 微指令。 (通常它会停止,直到 out-of-order back-end 中有空间发出完整的 4 组,而不是发出 non-full 组)。

很奇怪,我不知道确切的微体系结构限制是什么。由于循环只有 3 微指令,因此 4 微指令中的每个 issue-group 都超过了一个完整的迭代。因此一个问题组最多可以包含 3 个依赖 MOV。也许 Skylake 的设计有时会打破它,以允许更多 mov-elimination?

更新:实际上这对于 Skylake 上的 3-uop 循环是正常的。 uops_issued.stall_cycles 显示 HSW 和 SKL 发出一个简单的 3 uop 循环,没有 mov-elimination 与他们发出这个循环的方式相同。因此,更好的 mov-elimination 是 side-effect 出于某些其他原因拆分问题组。 (这不是瓶颈,因为 taken b运行ches 的执行速度不能超过每时钟 1 个,无论它们发出的速度有多快)。我仍然不知道为什么 SKL 不同,但我认为这没什么好担心的。


在不太极端的情况下,SKL 和 HSW 是相同的,都未能消除每 2 条 MOV 指令中的 0.3333:

.loop:
  mov eax, ecx
  dec eax
  mov ecx, eax

  sub ecx, 1
  jnz .loop
 2,333,434,710      cycles:u                  #    3.897 GHz                    
 5,000,000,185      instructions:u            #    2.14  insn per cycle         
 1,000,000,181      branches:u                # 1669.905 M/sec                  
 4,000,061,152      uops_issued_any:u         # 6679.720 M/sec                  
 2,333,374,781      uops_executed_thread:u    # 3896.513 M/sec                  
 1,000,000,942      lsd_cycles_4_uops:u       # 1669.906 M/sec                  

所有微指令以 4 为一组发出。任何连续的 4 微指令组都将恰好包含两个 MOV 微指令,它们是要消除的候选者。因为它显然成功地消除了某些 ccles,IDK 为什么它不能总是这样做。


Intel's optimization manual 表示尽早覆盖 mov-elimination 的结果可以释放微体系结构资源,因此它可以更频繁地成功,至少 movzx 是这样。请参见 示例 3-23。 Re-ordering 提高 Zero-Latency MOV 指令有效性的序列

所以也许它是用 limited-size table 的 ref-counts 进行内部跟踪的?当不再需要物理寄存器文件条目作为原始体系结构寄存器的值时,如果仍然需要它作为 mov 目标的值,则必须阻止释放物理寄存器文件条目。尽快释放 PRF 条目是关键,因为 PRF size can limit the out-of-order window 小于 ROB 大小。

我尝试了 Haswell 和 Skylake 上的示例,发现 mov-elimination 实际上在执行此操作时确实工作了更多的时间,但它实际上在总周期中稍微慢了一点,而不是更快。该示例旨在展示 IvyBridge 的优势,它可能在其 3 个 ALU 端口上存在瓶颈,但 HSW/SKL 仅在 dep 链中的资源冲突上存在瓶颈,并且似乎不需要 ALU 端口来实现更多movzx 条指令。

另请参阅 了解更多关于 mov-elimination 如何工作以及它是否适用于 xchg eax, ecx 的研究和猜测。 (实际上 xchg reg,reg 在 Intel 上是 3 个 ALU 微指令,但在 Ryzen 上是 2 个消除微指令。有趣的是猜测英特尔是否可以更有效地实现它。)


顺便说一句,作为 Haswell 勘误表的解决方法,Linux 在启用超线程时不提供 uops_executed.thread,仅提供 uops_executed.core。另一个核心肯定一直处于空闲状态,甚至没有定时器中断,because I took it offline with echo 0 > /sys/devices/system/cpu/cpu3/online。不幸的是,在内核的 perf 驱动程序 (PAPI) 决定在启动时启用 HT 之前,这无法完成,而我的戴尔笔记本电脑没有禁用 HT 的 BIOS 选项。所以我无法让 perf 在该系统上一次使用所有 8 个硬件 PMU 计数器,只有 4 个。:/