如果您需要从缓存行中读取任何字节,内核必须在 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 字节半的同一加载端口中需要 2 个周期。
- AMD 可能会因负载未对齐而跨越 16 字节或 32 字节边界而受到处罚。
- Zen2 之前的 AMD CPU 将 256 位(32 字节)的 AVX/AVX2 操作分成两个 128 位的一半,因此对于任何大小的相同成本规则仅适用于 AMD 上的 16 字节。或者在一些非常老的 CPU 上最多 8 个字节,这些 CPU 将 128 位向量分成两半,例如 Pentium-M 或 Bobcat。
- 整数负载的负载使用延迟可能比 SIMD 矢量负载低 1 或 2 个周期。但是您正在谈论进行更多加载的增量成本,因此无需等待新地址。 (可能只是来自同一个基址寄存器的不同立即位移。或者计算起来很便宜。)
我忽略了使用 512 位指令,甚至在某些 CPU 上使用 256 位指令会降低涡轮时钟的影响。
一旦您支付了缓存未命中的成本,该行的其余部分将在 L1d 缓存中处于热状态,直到某个其他线程想要写入它并且它们的 RFO(为所有权而读取)使您的行无效。强>
调用非内联函数 64 次而不是一次显然更昂贵,但我认为这只是你试图询问的一个坏例子。也许一个更好的例子是两个 int 加载与两个 __m128i 加载?
缓存未命中并不是唯一需要时间的事情,尽管它们很容易占据主导地位。但是,一个 call+ret 至少需要 4 个时钟周期(https://agner.org/optimize/ Haswell 的指令表显示每个 call/ret 有一个每 2 个时钟吞吐量,我认为这是正确的),所以 循环和调用在高速缓存行的 64 字节上执行 64 次函数至少需要 256 个时钟周期。这可能比某些 CPU 上的内核间延迟更长。如果它可以使用 SIMD 进行内联和自动矢量化,则超出缓存未命中的增量成本将大大降低,具体取决于它的功能。
在 L1d 中命中的负载非常便宜,例如每个时钟吞吐量 2 个。作为 ALU 指令的内存操作数的加载(而不需要单独的mov)可以作为与 ALU 指令相同的微指令的一部分进行解码,因此甚至不需要额外的前端带宽。
使用始终填充缓存行的更易于解码的格式可能对您的用例来说是一个胜利。除非这意味着循环更多次。当我说更容易解码时,我的意思是计算中的步骤更少,而不是看起来更简单的源代码(比如运行 64 次迭代的简单循环。)