在Intel x86中读取缓存行与缓存行的一部分

Cur*_*ous 2 c++ performance x86 assembly caching

这个问题可能没有确定的答案,但在这个领域寻找一般性的建议.如果这是偏离主题的,请告诉我.如果我的代码从不在当前CPU的L1缓存中的高速缓存行读取并且读取到远程高速缓存,则说该对象来自刚刚写入它的远程线程,因此具有修改模式的高速缓存行.读取整个缓存行与仅部分缓存行是否有任何增量成本?或者像这样的东西可以完全并行化吗?

例如,给定以下代码(假设foo()谎言是一些其他转换单元并且对优化器不透明,则不涉及LTO)

struct alignas(std::hardware_destructive_interference_size) Cacheline {
    std::array<std::uint8_t, std::hardware_constructive_interference_size> bytes;
};

void foo(std::uint8_t byte);
Run Code Online (Sandbox Code Playgroud)

这之间是否存在任何预期的性能差异

void bar(Cacheline& remote) {
  foo(remote.bytes[0]);
}
Run Code Online (Sandbox Code Playgroud)

还有这个

void bar(Cacheline& remote) {
    for (auto& byte : remote.bytes) {
        foo(byte);
    }
}
Run Code Online (Sandbox Code Playgroud)

或者这是什么东西很可能影响不大?在读取完成之前,整个高速缓存行是否转移到当前处理器?或者CPU可以并行读取和远程缓存行提取(在这种情况下,等待整个缓存行传输可能会产生影响)?


对于某些上下文:我处在一种情况,我知道可以将数据块设计为适合高速缓存行(压缩可能不会占用缓存未命中的CPU时间),或者可以压缩为了适应高速缓存行并尽可能紧凑,所以遥控器可以在不读取整个高速缓存行的情况下完成.两种方法都涉及截然不同的代码.试着找出我应该首先尝试哪一个以及一般建议在这里.

Pet*_*des 6

如果需要从高速缓存行读取任何字节,则核心必须在MESI共享状态下获取整个高速缓存行.在Haswell和之后,L2和L1d缓存之间的数据路径是64字节宽,(https://www.realworldtech.com/haswell-cpu/5/),所以字面上整条线路同时到达,在相同的时钟周期.仅读取低2字节与高字节和低字节,或字节0和字节32没有任何好处.

同样的事情在早期的CPU上基本上是正确的; 线路仍然作为一个整体发送,并且可能在2到8个时钟周期内到达.(AMD多插槽K10甚至可以通过HyperTransport在不同套接字上的内核之间发送线路时产生8字节边界的撕裂,因此它允许在发送或接收线路的周期之间发生高速缓存读取和/或写入.)

(当需要的字节到达时让负载开始在CPU架构术语中称为"早期重启" .相关的技巧是"关键字优先",其中DRAM的读取以启动需求负载所需的字开始突发这些都不是现代x86 CPU中的一个重要因素,数据路径与缓存线一样宽,或者接近它,一条线可能会在两个周期内到达.可能不值得发送一个字内线作为部分对缓存行的请求,即使在请求不仅仅来自HW预处理的情况下.)

多个缓存未命中加载到同一行不会占用额外的内存并行资源. 即使有序CPU通常也不会停止,直到某些东西试图使用未准备好的加载结果.在等待传入的缓存行时,乱序执行肯定会继续执行并完成其他工作.例如,在Intel CPU上,线路的L1d未命中分配线路填充缓冲区(LFB)以等待来自L2的传入线路.但是在行到达之前执行的同一缓存行的进一步加载只是将其加载缓冲区条目指向已经分配等待该行的LFB,因此它不会降低您有多个未完成的缓存未命中的能力(错过小姐)以后到其他线路.


任何不跨越缓存行边界的单个负载都具有与任何其他负载相同的成本,无论是1字节还是32字节.或AVX512 64字节.我能想到的几个例外是:

  • Nehalem之前的未对齐16字节加载:movdqu解码为额外的uop,即使地址对齐.
  • SnB/IvB 32字节AVX负载在相同的加载端口中占用2个周期,用于16字节的一半.
  • 对于未对齐的负载跨越16字节或32字节边界,AMD可能会受到一些惩罚.
  • 在Zen2之前的AMD CPU将256位(32字节)AVX/AVX2操作分成两个128位半部分,因此任何规模的相同成本规则仅适用于AMD的16个字节.或者在一些非常老的CPU上最多8个字节,将128位向量分成两半,如Pentium-M或Bobcat.
  • 整数加载可能比SIMD向量加载具有1或2个周期更低的负载使用延迟.但是你在谈论做更多负载的增量成本,所以没有新的地址可以等待.(大概是从同一个基址寄存器中的一个不同的立即位移.或者是便宜的东西来计算.)

我忽略了使用512位指令减少turbo时钟的影响,甚至在某些CPU上使用256位.


一旦你支付了高速缓存未命中的成本,该行的其余部分在L1d高速缓存中很热,直到其他线程想要写它并且他们的RFO(读取所有权)使你的线路无效.

调用非内联函数64次而不是一次显然更昂贵,但我认为这只是你想要问的一个坏例子.也许更好的例子是两个int负载与两个__m128i负载?

缓存未命中并不是花费时间的唯一因素,尽管它们很容易占据主导地位.但仍然,一个调用+ ret需要至少4个时钟周期(https://agner.org/optimize/ Haswell的指令表显示每个call/ret每2个时钟吞吐量有一个,我认为这是正确的),因此,在高速缓存行的64字节上循环和调用函数64次需要至少256个时钟周期.这可能比某些CPU上的核心间延迟更长.如果它可以使用SIMD进行内联和自动向量化,则超出高速缓存未命中的增量成本将大大减少,具体取决于它的作用.

在L1d中命中的负载非常便宜,例如每时钟吞吐量2个.作为ALU指令的内存操作数的加载(而不是需要单独的mov)可以作为ALU指令的同一uop进行解码,因此甚至不需要额外的前端带宽.


使用易于解码的格式总是填充缓存行可能是您的用例的胜利. 除非它意味着循环更多次.当我说更容易解码时,我的意思是计算中的步骤更少,而不是更简单的源代码(就像一个运行64次迭代的简单循环.)