次优高速缓存行预取的成本

Cur*_*ous 8 c++ performance x86 assembly prefetch

使用__builtin_prefetch(..., 1)内部函数(准备写入时的预取)完成后期预取的成本是多少?也就是说,在需求加载或写入需要它之前没有到达L1缓存的预取?

例如

void foo(std::uint8_t* line) {
    __builtin_prefetch(line + std::hardware_constructive_interference_size, 1);
    auto next_line = calculate_address_of_next_line(line);
    auto result = transform(line);
    write(next_line, result)
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,如果成本transform低于预取,那么这个代码最终会比没有预取的效率低吗?关于缓存预取的维基百科文章讨论了for循环的最佳步幅,但未提及该场景中次优预取的影响(例如,如果k太低会发生什么?).

这是否足够流水线以至于次优预取无关紧要?出于这个问题的目的,我只考虑Intel x86(Broadwell时代的处理器).

Bee*_*ope 7

让我们调用您所指的延迟预取的预取类型:在需求加载或使用相同缓存行的存储完全隐藏缓存未命中的延迟之前,预取不会充分发生.这与过早的预取相反,其中预取发生在远离请求访问的情况下,它在访问发生之前从至少某些级别的高速缓存中被驱逐.

与根本不进行预取相比,这种后期预取的成本可能非常小,为零或为负.

让我们关注负面部分:即预取有助于即使它迟到的情况.如果我正确理解了您的问题,您会考虑在需要"错过"或无效的负载之前未到达的预取.然而,情况并非如此:一旦预取请求开始,时钟开始计时以完成内存访问,并且如果在完成之前发生需求加载,则工作不会丢失.例如,如果您的内存访问需要100 ns,但需求访问仅在预取后20 ns发生,那么预取是"太晚",因为没有隐藏完整的100 ns延迟,但是20 ns花费在预取仍然有用:它将需求访问延迟减少到大约80 ns.

也就是说,后期预取不是二进制条件:它的范围从稍晚一点(例如,在访问之前90 ns发出预取,延迟为100 ns),或者实际上很晚(几乎在消费访问之前) .在大多数情况下,即使是相当晚的预取也可能有所帮助,假设内存延迟首先是算法的瓶颈.

成本

现在让我们考虑一个完全无用的预取的情况(即,在访问之前立即发出,因此如果预取不存在则可以在其位置发出访问权限) - 成本是多少?在大多数现实场景中,成本可能非常小:要处理的额外指令,AGU上的一些小额外压力,以及在将后续访问与飞行中预取2匹配时可能会浪费少量资金.

由于假设由于错过了高速缓存或DRAM的外层而采用了预取,并且transform函数中的工作足以隐藏一些延迟,因此这一附加指令的相对成本可能非常高.小.

当然,这都是在假设附加预取是单个指令的情况下进行的.在某些情况下,您可能不得不在某种程度上组织代码以允许预取或执行一些重复计算以允许在适当的位置进行预取.在这种情况下,成本方面可能相应地更高.

M和E国家

最后,对于写入访问和具有写入意图的预取存在另外的行为,这意味着在某些情况下,即使是完全无用的预取(即,在第一次访问之前)也是有用的 - 当第一次访问是读取时.

如果首先读取给定的行,然后再写入,则核心可以使该行处于E(xclusive)一致性状态,然后在第一次需要进行另一次往返到某个级别的高速缓存以使其处于M状态.在第一次访问之前使用具有写意图的预取将避免第二次往返,因为该行将在第一次带入M状态.这种优化的效果一般很难量化,尤其是因为写入通常是缓冲的,并且不构成依赖链的一部分(在存储转发之外).


2我在这里使用故意模糊的术语"浪费精力",因为它不是很清楚这是否具有性能或功耗,或者只是一些不会增加操作延迟的额外工作.一个可能的成本是触发初始L1未命中的负载具有特殊状态并且可以在不进行到L1的另一次往返的情况下接收其结果.在紧接着加载之后的预取的情况下,负载可能不会获得特殊状态,这可能会略微增加成本.但是,这个问题是关于商店而不是负载.

  • @好奇-关于存储,我的意思是说加载具有寄存器输入(地址)和寄存器输出(加载值),因此从某种意义上讲,它们就像大多数ALU指令一样,具有输入和输出。因此,谈论负载的延迟是正确的:如果您有一系列负载,第N + 1个负载的输入取决于第N个负载的输出(也称为指针追逐),则将测量负载延迟,因为每个负载在上一个操作完成之前,无法开始加载。同样,依赖于负载的任何其他指令只有在完成后才能开始。 (2认同)
  • 因此,错过的负载会减慢所有相关指令的速度。存储不是这样的:它们有两个输入(要存储的数据,要存储的地址),但根本没有寄存器输出。因此,从寄存器依赖关系的意义上讲,以后没有任何指令“依赖”存储,因此,您无法建立与上述针对存储的指针追逐测试等效的方法,甚至谈论“存储延迟”的定义也很模糊。从某种意义上说,存储是一种火灾:忘记了:它们进入缓冲区,可能需要很长时间才能到达DRAM(也许直到关闭主机后它们才会保留在缓存中),但是看不到这种影响。 (2认同)
  • @Curious - 这不是毫无意义的,只是与负载相比它的工作方式不太清楚。负载相当“简单”:如果您提前 x 个周期开始负载,您可能会大约从观察到的负载延迟中减少 x 个周期,这会对性能产生可预测的影响(即使这比分析听起来更复杂,因为 -顺序和其他效果,但仍然是基本思想)。存储更难,因为缓冲区已经作为预取机制:存储进入存储缓冲区,“一段时间后”他们最终提交到 L1(这甚至可以在...... (2认同)