assembly - 为什么每次迭代的微指令数会随着流加载的步幅而增加?
问题描述
考虑以下循环:
.loop:
add rsi, OFFSET
mov eax, dword [rsi]
dec ebp
jg .loop
其中OFFSET
是一些非负整数,并rsi
包含指向该bss
部分中定义的缓冲区的指针。这个循环是代码中唯一的循环。也就是说,它在循环之前没有被初始化或触摸。据推测,在 Linux 上,缓冲区的所有 4K 虚拟页面将按需映射到同一个物理页面。因此,缓冲区大小的唯一限制是虚拟页面的数量。所以我们可以很容易地试验非常大的缓冲区。
该循环由 4 条指令组成。在 Haswell 的融合和非融合域中,每条指令都被解码为单个 uop。的连续实例之间也存在循环携带的依赖关系add rsi, OFFSET
。因此,在负载总是在 L1D 中命中的空闲条件下,循环应该以每次迭代大约 1 个周期执行。对于小的偏移量(步幅),这要归功于基于 IP 的 L1 流式预取器和 L2 流式预取器。但是,两个预取器都只能在 4K 页面内进行预取,并且 L1 预取器支持的最大步幅为 2K。因此,对于小步幅,每 4K 页面应该有大约 1 个 L1 未命中。随着步幅的增加,L1 未命中和 TLB 未命中的总数会增加,性能也会相应下降。
下图显示了步幅在 0 到 128 之间的各种有趣的性能计数器(每次迭代)。请注意,所有实验的迭代次数都是恒定的。只有缓冲区大小会更改以适应指定的步幅。此外,仅计算用户模式性能事件。
这里唯一奇怪的是退休的微指令的数量随着步伐的增加而增加。对于步幅 128,它从每次迭代 3 微秒(如预期)到 11 微秒。这是为什么呢?
如下图所示,步幅越大,事情就越奇怪。在此图中,步幅范围从 32 到 8192,增量为 32 字节。首先,退出指令的数量以 4096 字节的步幅从 4 线性增加到 5,之后它保持不变。加载 uops 的数量从 1 增加到 3,并且每次迭代的 L1D 加载命中数保持 1。对于我来说,只有 L1D 加载未命中的数量才有意义。
较大步幅的两个明显影响是:
- 执行时间增加,因此会发生更多的硬件中断。但是,我正在计算用户模式事件,因此中断不应干扰我的测量。我还用
taskset
or重复了所有实验,nice
并得到了相同的结果。 - 页面遍历和页面错误的数量增加。(我已经验证了这一点,但为了简洁起见,我将省略这些图表。)页面错误由内核在内核模式下处理。根据这个答案,页面遍历是使用专用硬件(在 Haswell 上?)实现的。尽管答案所基于的链接已失效。
为了进一步研究,下图显示了微码辅助的微指令数。与其他性能事件一样,每次迭代的微码辅助 uops 数量会增加,直到在步幅 4096 处达到最大值。对于所有步幅,每个 4K 虚拟页面的微码辅助 uops 数量为 506。“额外的 uops”线绘制了退役 uops 的数量减去 3(每次迭代的预期 uops 数量)。
该图显示,对于所有步幅,额外微码数略大于微码辅助微码数的一半。我不知道这意味着什么,但它可能与页面漫游有关,并且可能是观察到扰动的原因。
为什么即使每次迭代的静态指令数量相同,每次迭代的退役指令和微指令的数量也会随着更大的步幅而增加?干扰来自哪里?
下图绘制了每次迭代的周期数与每次迭代不同步幅的退役微指令数。周期数的增加速度远快于退役的微指令数。通过使用线性回归,我发现:
cycles = 0.1773 * stride + 0.8521
uops = 0.0672 * stride + 2.9277
取两个函数的导数:
d(cycles)/d(stride) = 0.1773
d(uops)/d(stride) = 0.0672
这意味着周期数增加 0.1773,退休的微指令数增加 0.0672,步幅每增加 1 个字节。如果中断和页面错误确实是扰动的(唯一)原因,那么这两种速率不应该非常接近吗?
解决方案
您在许多性能计数器中反复看到的效果,其中值线性增加,直到步幅 4096 之后它保持不变,如果您假设该效果纯粹是由于随着步幅的增加而增加页面错误,则完全有意义。页面错误会影响观察到的值,因为在存在中断、页面错误等情况下,许多计数器并不准确。
例如,instructions
当您从步幅 0 前进到 4096 时,计数器从 4 变为 5。我们从其他来源知道,Haswell 上的每个页面错误都会在用户模式下计算一条额外指令(在内核模式下也会额外计算一条指令) .
因此,我们期望的指令数是循环中 4 条指令的基数,加上基于每个循环发生多少页错误的指令的一部分。如果我们假设每个新的 4 KiB 页面都会导致页面错误,那么每次迭代的页面错误数为:
MIN(OFFSET / 4096, 1)
由于每个页面错误都会计算一条额外的指令,因此我们有预期的指令计数:
4 + 1 * MIN(OFFSET / 4096, 1)
这与您的图表完全一致。
因此,一次为所有计数器解释了斜率图形的粗略形状:斜率仅取决于每个页面错误的过度计数量。那么剩下的唯一问题是为什么页面错误会以您确定的方式影响每个计数器。我们已经介绍过instructions
,但让我们看看其他的:
MEM_LOAD_UOPS.L1_MISS
每页只有 1 次未命中,因为只有触及下一页的负载才会丢失任何内容(它需要出错)。我实际上并不同意 L1 预取器不会导致其他未命中:我认为如果您关闭预取器,您会得到相同的结果。我认为您不会再有 L1 未命中,因为相同的物理页面支持每个虚拟页面,并且一旦您添加了 TLB 条目,所有行都已经在 L1 中(第一次迭代将丢失 - 但我猜您正在进行多次迭代)。
MEM_UOPS_RETIRED.ALL_LOADS
这显示每个页面错误 3 uops(2 额外)。
我不是 100% 确定这个事件在 uop 重放的情况下是如何工作的。它是否总是根据指令计算固定数量的微指令,例如,您在 Agner 的指令 -> 微指令表中看到的数字?或者它是否计算代表指令发送的实际微指令数?这通常是相同的,但是当它们在不同的缓存级别丢失时,加载重放它们的微指令。
例如,我发现在 Haswell 和 Skylake 2上,当 L1 中的负载丢失但在 L2 中命中时,您会看到负载端口(端口 2 和端口 3)之间总共有 2 个微指令。据推测,发生的情况是 uop 是在假设它将在 L1 中命中的情况下分派的,并且当这没有发生时(当调度程序预期它时结果还没有准备好),它会以预计 L2 命中的新时间来重放。这是“轻量级”的,因为它不需要任何类型的管道清除,因为没有执行错误路径指令。
同样,对于 L3 未命中,我观察到每次负载 3 uops。
鉴于此,假设新页面上的未命中导致加载 uop 被重播两次(正如我所观察到的)似乎是合理的,并且这些 uop 出现在MEM_UOPS_RETIRED
计数器中。有人可能会合理地争辩说,重放的微指令没有退役,但在某种意义上,退役与指令的关联比微指令更重要。也许这个计数器可以更好地描述为“与退役加载指令相关的调度微指令”。
UOPS_RETIRED.ALL
和IDQ.MS_UOPS
剩下的奇怪之处是与每个页面相关的大量微指令。这似乎完全有可能与页面错误机制有关。您可以尝试在 TLB 中遗漏的类似测试,但不会出现页面错误(确保页面已经填充,例如,使用mmap
with MAP_POPULATE
)。
MS_UOPS
和之间的区别UOPS_RETIRED
似乎并不奇怪,因为有些微指令可能不会退役。也许他们也算在不同的域中(我忘了UOPS_RETIRED
是融合域还是非融合域)。
在这种情况下,用户和内核模式计数之间也可能存在泄漏。
循环与 uop 导数
在您问题的最后一部分中,您表明周期与偏移的“斜率”比退役 uops 与偏移的斜率大约 2.6 倍。
如上所述,这里的效果在 4096 处停止,我们再次预计这种效果完全是由于页面错误造成的。所以斜率的差异仅仅意味着页面错误的周期是 uops 的 2.6 倍。
你说:
如果中断和页面错误确实是扰动的(唯一)原因,那么这两种速率不应该非常接近吗?
我不明白为什么。微指令和周期之间的关系可能相差很大,可能相差三个数量级:CPU 可能每个周期执行四个微指令,或者执行单个微指令可能需要 100 秒的周期(例如缓存缺失加载)。
每 uop 2.6 个周期的值正好在这个大范围的中间,我并不觉得奇怪:它有点高(如果您在谈论优化的应用程序代码,“效率低下”)但这里我们正在谈论页面故障处理是完全不同的事情,所以我们预计会有很长的延迟。
过度计数的研究
由于页面错误和其他事件而对过度计数感兴趣的任何人都可能对这个 github 存储库感兴趣,该存储库对各种 PMU 事件的“确定性”进行了详尽的测试,并且已经注意到许多这种性质的结果,包括在 Haswell 上。然而,它并没有涵盖哈迪在这里提到的所有计数器(否则我们已经有了答案)。这是相关的论文和一些更易于使用的相关幻灯片- 他们特别提到每个页面错误都会产生一个额外的指令。
以下是英特尔结果的引述:
Conclusions on the event determinism:
1. BR_INST_RETIRED.ALL (0x04C4)
a. Near branch (no code segment change): Vince tested
BR_INST_RETIRED.CONDITIONAL and concluded it as deterministic.
We verified that this applies to the near branch event by using
BR_INST_RETIRED.ALL - BR_INST_RETIRED.FAR_BRANCHES.
b. Far branch (with code segment change): BR_INST_RETIRED.FAR_BRANCHES
counts interrupts and page-faults. In particular, for all ring
(OS and user) levels the event counts 2 for each interrupt or
page-fault, which occurs on interrupt/fault entry and exit (IRET).
For Ring 3 (user) level, the counter counts 1 for the interrupt/fault
exit. Subtracting the interrupts and faults (PerfMon event 0x01cb and
Linux Perf event - faults), BR_INST_RETIRED.FAR_BRANCHES remains a
constant of 2 for all the 17 tests by Perf (the 2 count appears coming
from the Linux Perf for counter enabling and disabling).
Consequently, BR_INST_RETIRED.FAR_BRANCHES is deterministic.
因此,您期望每个页面错误都有一条额外的指令(特别是分支指令)。
1 在许多情况下,这种“不精确性”仍然是确定性的——因为在存在外部事件的情况下,过度计数或计数不足总是以相同的方式表现,因此如果您还跟踪有多少相关事件已经发生。
2我并不是要把它限制在这两种微架构上:它们恰好是我测试过的那种。
推荐阅读
- python - Python 'getopt' 语法,允许可选的命令行参数,longopts 无法识别
- python - to_csv 将每个插入数据的列标签存储到 csv 文件
- python - python pandas数据框错位
- rust - 无法指定生命周期参数来解决编译错误
- java - 无法将 protobuf 消息类型转换为镶木地板
- c++ - 当相机远离屏幕时,glDrawArraysInstanced 的行为很奇怪
- mysql - mysql远程连接digitalocean
- python - 将 QWidgets 堆叠在一起,两者都可见
- php - 通过从每个数组中一次提取 3 个元素将两个数组合并为一个平面数组
- ios - Swift - 从实时摄像机中获取静止帧并将它们输入 Vision 框架