c++ - 在Intel x86中读取缓存行与部分缓存行

标签 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时间),或者可以对其进行压缩可以容纳在缓存行中并尽可能紧凑,因此 Remote 可以在不读取整个缓存行的情况下进行操作。两种方法都将涉及实质上不同的代码。只是想弄清楚我应该首先尝试哪个,以及这里的一般建议是什么。

最佳答案

如果您需要从高速缓存行中读取任何字节,则内核必须以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字节。我能想到的一些异常(exception)是:

在Nehalem之前

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

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

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

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

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

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

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

    关于c++ - 在Intel x86中读取缓存行与部分缓存行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54800121/

    相关文章:

    performance - 平衡 KD 树 : Which approach is more efficient?

    javascript - 从 UI/UX 开发中抛弃 jQuery

    asp.net - Distributed Cache/Session 我应该转向哪里?

    http - 对于可以更改的内容,哪些是与缓存相关的最佳 HTTP header ?

    c++ - 如何在 Linux 中监视子进程状态

    c++ - 我该如何编写这段代码?

    c++ - *this 和 const 成员函数

    c# - 通过编写 C dll 来加速 C# 中的数学代码?

    c++ - 如何进一步压缩 JPEG 以在 Windows 上以 C/C++ 发送 TCP 流?

    symfony - Doctrine 缓存无法识别的选项错误