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

时间:2019-02-21 05:52:07

标签: c++ performance caching assembly x86

这个问题可能没有一个定论的答案,而是在这方面寻求一般建议。让我知道这是否是题外话。如果我有代码从不在当前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);

两者之间是否有预期的性能差异

void bar(Cacheline& remote) {
  foo(remote.bytes[0]);
}

还有这个

void bar(Cacheline& remote) {
    for (auto& byte : remote.bytes) {
        foo(byte);
    }
}

这是否最有可能影响很小?在读取完成之前,是否将整个高速缓存行转移到当前处理器?还是CPU可以并行执行读取和远程高速缓存行提取(在这种情况下,等待整个高速缓存行传输可能会产生影响)?


对于某些情况:我处于一种情况,即我知道可以将数据块设计为适合高速缓存行(压缩可能不会占用与高速缓存未命中一样多的CPU时间),或者可以压缩以适合高速缓存行并尽可能紧凑,因此远程操作无需读取整个高速缓存行。两种方法都将涉及实质上不同的代码。只想弄清楚我应该首先尝试哪个,以及这里的一般建议。

1 个答案:

答案 0 :(得分:6)

如果您需要从缓存行中读取任何字节,则内核必须以MESI Shared状态获取整个缓存行。在Haswell及更高版本上,L2和L1d缓存之间的数据路径为64字节宽(https://www.realworldtech.com/haswell-cpu/5/),因此整行实际上是在相同的时钟周期内同时到达的。仅读取低2个字节与高和低字节或字节0和字节32相比没有好处。

在较早的CPU上,这基本上是相同的;线路仍将作为整体发送,并且可能需要2到8个时钟周期的脉冲串到达。 (AMD多路K10甚至可以通过HyperTransport在不同套接字上的内核之间发送线路时创建tearing across 8-byte boundaries,因此它允许在发送或接收线路的周期之间发生缓存读取和/或写入。)

(在需要的字节到达时让加载开始,称为“提前重启” in CPU-architecture terminology。一个相关的技巧是“关键字优先”,其中从DRAM读取时以请求所需要的字开始突发在现代x86 CPU中,这两者都不是影响高速缓存行宽或接近高速缓存行的数据路径的重要因素,因为高速缓存行的数据路径可能接近2个周期。行作为缓存行请求的一部分,即使在请求不仅仅是来自硬件采购的情况下也是如此。

同一行上的多个高速缓存未命中加载不会占用额外的内存并行资源。即使有序的CPU通常也不会停止运行,直到某些东西试图使用不符合要求的加载结果还没准备好在等待传入的高速缓存行时,乱序执行肯定可以继续进行并完成其他工作。例如,在Intel CPU上,线路的L1d丢失会分配线路填充缓冲区(LFB)以等待来自L2的传入线路。但是,在行到达之前执行的同一高速缓存行中进一步加载,只需将其加载缓冲区条目指向已经分配用于等待该行的LFB,这样就不会降低您发生多个未决高速缓存未命中的能力(错过),然后转到其他行。


任何不超过高速缓存行边界的负载都与其他负载具有相同的开销,无论是1字节还是32字节。或使用AVX512为64字节。我能想到的一些例外是:

  • 在Nehalem之前未对齐的16字节加载:movdqu解码为额外的内容,即使地址 已对齐。
  • SnB / IvB 32字节AVX加载在同一个加载端口中进行两个周期,分别为16字节的一半。
  • AMD可能会对未对齐负载的16字节或32字节边界进行惩罚。
  • Zen2之前的AMD CPU将256位(32字节)AVX / AVX2操作分成两个128位,因此,任何大小的相同成本规则仅适用于AMD最多16个字节。在某些非常老的CPU(例如Pentium-M或Bobcat)将128位向量分成两半的情况下,最多可以有8个字节。
  • 整数负载可能比SIMD向量负载具有1或2个周期的负载使用延迟。但是您正在谈论增加负载的增量成本,因此没有新的地址可以等待。 (大概是与同一个基址寄存器不同的立即移位。或者计算起来很便宜。)

我忽略了在某些CPU上使用512位指令甚至是256位指令所导致的Turbo时钟减少的影响。


一旦您支付了高速缓存未命中的费用,其余行在L1d高速缓存中就很热,直到其他线程想要写入它并且其RFO(所有权读取)使该行无效。

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

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

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


使用始终填充高速缓存行的易于解码的格式可能是您的用例之选。除非这意味着要循环更多次。当我说容易解码时,我的意思是计算的步骤更少,而不是看起来更简单的源代码(例如运行64次迭代的简单循环)。