【发布时间】:2019-03-02 19:09:25
【问题描述】:
考虑以下循环:
.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或nice重复了所有实验,得到了相同的结果。 - 页面遍历和页面错误的数量增加。 (我已经验证了这一点,但为了简洁起见,我将省略这些图表。)页面错误由内核在内核模式下处理。根据this 的回答,页面遍历是使用专用硬件(在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
这意味着每增加 1 个字节,周期数就会增加 0.1773,而退役的微指令数会增加 0.0672。如果中断和页面错误确实是(唯一的)扰动原因,那么这两种速率不应该非常接近吗?
【问题讨论】:
-
是的,自 P6 以来,页面浏览使用专用硬件,而不是微编码的 uops。 @Bee 说 L1 错过了执行额外 uop 的“成本”,显然它们会被重放或其他东西。 AVX 512 improvements?.
-
关于回放,对于您错过的每一级缓存,似乎还有一个 p23 uop。即,L1 中的命中为 1 uop,L2 中的命中为 2 微秒,L3 中的命中为 3 微秒(也许这就是它停止的地方)。我想也许发生的事情是调度程序总是乐观的:它不知道你会命中什么级别的缓存,所以它在每一次机会时都会唤醒依赖操作以获得最佳命中:4/ L1 为 5 个周期,L2 为 12 个周期,依此类推。所以每次你错过你都会得到一个额外的 uop。在其他情况下,您也会得到很多 uops,例如,如果 4 周期快速路径失败。
-
@BeeOnRope:我对 L3 感到惊讶,延迟取决于环形总线争用,因此调度程序很难预测预期结果的确切周期。如果它基于在实际准备好之前的一个周期收到传入数据的通知,则不会出现误报。 (或者,即使未命中也会有通知,因此性能计数器可以在检测到 l3 未命中时而不是在 DRAM 结果到达时计算 l3 命中与未命中?)
-
在 Linux 上,当页面错误发生时,操作系统可能会更新页面表以获取其他“附近”页面(在我的系统上,额外的 15 个页面)(如果它们是常驻的)。这意味着页面错误在我的系统上减少了 16 倍,因为每个错误实际上增加了 16 个页面。这适用于文件支持的页面,但可能不适用于特殊的 bss(隐式映射零页面或类似的东西)。
-
@PeterCordes 和 Hadi - 关于重播内容的另一个更新 - 经过更多检查后,我发现了发生了什么:通常重播的是 dependent 操作,这就是为什么插入一些 ALU 操作会阻止我看到它(因为我没有在看
p0156uops)。因此,基本上,当负载馈入负载时,只会重播负载,因为它是唯一的依赖操作。如果您之后有 ALU 操作,则将重放 ALU 操作。有时会重放多个微指令,包括非直接依赖的微指令,似乎会在加载的一个周期内执行的微指令被重放。
标签: assembly x86 cpu-architecture intel-pmu