【发布时间】:2018-06-06 00:40:13
【问题描述】:
我想编写一个负载基准测试,它以编译时已知的跨度跨过给定的内存区域,在该区域的末尾包装(2 的幂),并且使用尽可能少的非加载指令可能。
例如,给定 4099 的步幅,rdi 中的迭代计数和rsi 中指向内存区域的指针,“有效”的东西是:
%define STRIDE 4099
%define SIZE 128 * 1024
%define MASK (SIZE - 1)
xor ecx, ecx
.top:
mov al, [rsi + rcx]
add ecx, STRIDE
and ecx, MASK
dec rdi
jnz .top
问题是有 4 个非加载指令只是为了支持单次加载,处理步幅加法、屏蔽和循环终止检查。此外,还有一个通过ecx进行的2循环依赖链。
我们可以稍微展开一下,以将循环终止检查成本降低到接近零,并分解依赖链(这里展开 4 倍):
.top:
lea edx, [ecx + STRIDE * 0]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 1]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 2]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
lea edx, [ecx + STRIDE * 3]
and edx, MASK
movzx eax, BYTE [rsi + rdx]
add ecx, STRIDE * 4
dec rdi
jnz .top
但是,这对于处理包裹步幅的add 和and 操作没有帮助。例如,对于包含 L1 的区域,上述基准报告为 0.875 个周期/负载,但我们知道正确答案是 0.5(每个周期两个负载)。 0.875 来自 15 总 uops / 4 uops 周期,即,我们受限于 uop 吞吐量的 4-wide 最大宽度,而不是负载吞吐量。
知道如何有效地展开循环以消除步幅计算的大部分成本吗?
【问题讨论】:
-
我想 CPU 最终会与负载并行调度这些指令。
-
@OliverCharlesworth - 有时是的,但它无法隐藏原始示例中计算的延迟,当负载每个吞吐量为 1 个周期时,但通过
ecx的依赖链需要 2 个周期,所以当区域适合 L1 或 L2 时,实际上是支配循环的算术。展开的版本更好,但我仍然希望指令更少(特别是,并非所有拱门都足够宽以并行安排所有其他指令)。 -
我不确定您是否会受到延迟而不是吞吐量的阻碍 - 因为每次写入
al都会造成 WAW 危害。 -
对于某些步幅,您可以循环直到
and ecx,MASK为零,然后使用迭代计数器(如果这是基准测试并且步幅与掩码达到有趣的重复模式)。对于手动编译时跨步,您可以展开循环,直到需要第一个掩码(之前只做add)......然后这个展开的循环与前一个and上的另一个add将很快和不准确的猜测足以循环,永远不会离开设计区域,但需要数学证明/验证。 -
@Ped7g - 这是一个非常好的观点。事实上,我选择
STRIDE总是一个较大的素数,所以它总是相对于迭代计数相对素数,所以我可以循环直到ecx变为零(我需要仔细选择我的展开计数,以便它实际上发生在在迭代结束时,我认为展开因子是迭代计数的除数就足够了)。尴尬的部分是它使代码变得更简单,因为它有一个精确命中的显式迭代计数(没有“尾部处理”循环),但我也许可以适应这种策略。
标签: assembly optimization x86 nasm