【发布时间】:2021-12-19 15:38:55
【问题描述】:
考虑以下在 ARM Cortex-A72 处理器上运行的代码(优化指南 here)。我已经包含了我期望的每个执行端口的资源压力:
| Instruction | B | I0 | I1 | M | L | S | F0 | F1 |
|---|---|---|---|---|---|---|---|---|
.LBB0_1: |
||||||||
ldr q3, [x1], #16 |
0.5 | 0.5 | 1 | |||||
ldr q4, [x2], #16 |
0.5 | 0.5 | 1 | |||||
add x8, x8, #4 |
0.5 | 0.5 | ||||||
cmp x8, #508 |
0.5 | 0.5 | ||||||
mul v5.4s, v3.4s, v4.4s |
2 | |||||||
mul v5.4s, v5.4s, v0.4s |
2 | |||||||
smull v6.2d, v5.2s, v1.2s |
1 | |||||||
smull2 v5.2d, v5.4s, v2.4s |
1 | |||||||
smlal v6.2d, v3.2s, v4.2s |
1 | |||||||
smlal2 v5.2d, v3.4s, v4.4s |
1 | |||||||
uzp2 v3.4s, v6.4s, v5.4s |
1 | |||||||
str q3, [x0], #16 |
0.5 | 0.5 | 1 | |||||
b.lo .LBB0_1 |
1 | |||||||
| Total port pressure | 1 | 2.5 | 2.5 | 0 | 2 | 1 | 8 | 1 |
虽然uzp2 可以在F0 或F1 端口上运行,但我选择将其完全归因于F1,因为F0 上的压力很高,而F1 上的压力为零,除了这条指令。
循环迭代之间没有依赖关系,除了循环计数器和数组指针;与循环体的其余部分所花费的时间相比,这些问题应该很快得到解决。
因此,我的直觉是这段代码应该受到吞吐量限制,并且考虑到最严重的压力是在 F0 上,每次迭代运行 8 个周期(除非它遇到解码瓶颈或缓存未命中)。考虑到流式访问模式以及阵列很适合 L1 缓存的事实,后者不太可能发生。至于前者,考虑到优化手册第 4.1 节列出的约束,我预测循环体只需 8 个周期即可解码。
然而,微基准测试表明循环体的每次迭代平均需要 12.5 个周期。如果不存在其他合理的解释,我可能会编辑该问题,包括有关我如何对该代码进行基准测试的更多详细信息,但我相当确定这种差异不能仅归因于基准测试工件。此外,我尝试增加迭代次数,以查看性能是否由于启动/冷却效应而提高到渐近极限,但对于上面显示的 128 次迭代的选定值,它似乎已经这样做了。
手动展开循环以在每次迭代中包含两个计算,从而将性能降低到 13 个循环;但是,请注意,这也会重复加载和存储指令的数量。有趣的是,如果将双倍的加载和存储替换为单个 LD1/ST1 指令(双寄存器格式)(例如 ld1 { v3.4s, v4.4s }, [x1], #32),那么每次迭代的性能提高到 11.75 个周期。将循环进一步展开为每次迭代 4 次计算,同时使用 LD1/ST1 的四寄存器格式,将性能提高到每次迭代 11.25 个周期。
尽管进行了改进,但性能仍与我仅从资源压力方面预期的每次迭代 8 个周期相去甚远。即使 CPU 进行了错误的调度调用并向 F0 发出了uzp2,修改资源压力表也将表明每次迭代需要 9 个周期,与实际测量结果相距甚远。那么,是什么导致这段代码运行得比预期慢得多?我在分析中遗漏了哪些影响?
编辑:正如所承诺的,更多的基准测试细节。我为预热运行循环 3 次,说 n = 512 运行 10 次,然后为 n = 256 运行 10 次。我取 n = 512 运行的最小循环计数并从最小值中减去 n = 256。差异应该给我运行 n = 256 需要多少个周期,同时取消固定设置成本(代码未显示)。此外,这应确保所有数据都在 L1 I 和 D 缓存中。通过直接读取循环计数器 (pmccntr_el0) 进行测量。上述测量策略应抵消任何开销。
【问题讨论】:
-
ARM 性能数据不能包含芯片(非 arm 制造)或系统(非 arm 制造)问题。而且它是流水线的,因此性能预计无法预测。您是否考虑了所有这些因素?
-
您是否尝试过代码相对于内存空间的不同对齐方式来解释获取行? (缓存与否)
-
@Jake'Alquimista'LEE:ROB 大小与隐藏一条慢速指令(如缓存未命中)有关。要重叠长的 dep 链,您还需要一个足够大的 scheduler (RS) 来处理尚未执行的指令。 (见Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths)。大型调度程序比大型 ROB 消耗更多功率,因为它没有按顺序分配/回收,并且必须在每个周期扫描最旧的就绪指令。一些 CPU 使用统一的调度程序,其他 CPU 使用每个端口的调度队列。
-
所以我想说我们不能仅仅基于 ROB 大小就排除 OoO 执行限制。调度器可能足够大,但也可能不够。 (此外,物理寄存器文件的大小可能是另一个限制(例如x86 experiments,但它通常接近 ROB 大小,并且此循环混合了整数和向量 regs,因此可能将其 PRF 需求分布在两个寄存器文件上,假设 Cortex-A72 与大多数设计一样,具有独立的 int 与 FP/SIMD 寄存器文件。)
-
@PeterCordes 根据this article,Cortex-A72 中的保留站是按端口而不是全局的。猜猜我将不得不拆除并重建我为这个核心编写代码的心智模型,因为它应该只能看起来像F0 端口中最多前 8 条指令。
标签: performance assembly optimization arm neon