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