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时代的处理器).
让我们调用您所指的延迟预取的预取类型:在需求加载或使用相同缓存行的存储完全隐藏缓存未命中的延迟之前,预取不会充分发生.这与过早的预取相反,其中预取发生在远离请求访问的情况下,它在访问发生之前从至少某些级别的高速缓存中被驱逐.
与根本不进行预取相比,这种后期预取的成本可能非常小,为零或为负.
让我们关注负面部分:即预取有助于即使它迟到的情况.如果我正确理解了您的问题,您会考虑在需要"错过"或无效的负载之前未到达的预取.然而,情况并非如此:一旦预取请求开始,时钟开始计时以完成内存访问,并且如果在完成之前发生需求加载,则工作不会丢失.例如,如果您的内存访问需要100 ns,但需求访问仅在预取后20 ns发生,那么预取是"太晚",因为没有隐藏完整的100 ns延迟,但是20 ns花费在预取仍然有用:它将需求访问延迟减少到大约80 ns.
也就是说,后期预取不是二进制条件:它的范围从稍晚一点(例如,在访问之前90 ns发出预取,延迟为100 ns),或者实际上很晚(几乎在消费访问之前) .在大多数情况下,即使是相当晚的预取也可能有所帮助,假设内存延迟首先是算法的瓶颈.
现在让我们考虑一个完全无用的预取的情况(即,在访问之前立即发出,因此如果预取不存在则可以在其位置发出访问权限) - 成本是多少?在大多数现实场景中,成本可能非常小:要处理的额外指令,AGU上的一些小额外压力,以及在将后续访问与飞行中预取2匹配时可能会浪费少量资金.
由于假设由于错过了高速缓存或DRAM的外层而采用了预取,并且transform函数中的工作足以隐藏一些延迟,因此这一附加指令的相对成本可能非常高.小.
当然,这都是在假设附加预取是单个指令的情况下进行的.在某些情况下,您可能不得不在某种程度上组织代码以允许预取或执行一些重复计算以允许在适当的位置进行预取.在这种情况下,成本方面可能相应地更高.
最后,对于写入访问和具有写入意图的预取存在另外的行为,这意味着在某些情况下,即使是完全无用的预取(即,在第一次访问之前)也是有用的 - 当第一次访问是读取时.
如果首先读取给定的行,然后再写入,则核心可以使该行处于E(xclusive)一致性状态,然后在第一次需要进行另一次往返到某个级别的高速缓存以使其处于M状态.在第一次访问之前使用具有写意图的预取将避免第二次往返,因为该行将在第一次带入M状态.这种优化的效果一般很难量化,尤其是因为写入通常是缓冲的,并且不构成依赖链的一部分(在存储转发之外).
2我在这里使用故意模糊的术语"浪费精力",因为它不是很清楚这是否具有性能或功耗,或者只是一些不会增加操作延迟的额外工作.一个可能的成本是触发初始L1未命中的负载具有特殊状态并且可以在不进行到L1的另一次往返的情况下接收其结果.在紧接着加载之后的预取的情况下,负载可能不会获得特殊状态,这可能会略微增加成本.但是,这个问题是关于商店而不是负载.