术语:负载不会产生 RFO,它不需要所有权。它仅发送共享数据的请求。多个内核可以从同一个物理地址并行读取,每个内核在其 L1d 缓存中都有一个热副本。
但是,写入该行的其他内核将发送 RFO,这会使我们缓存中的共享副本无效,是的,可能会在读取缓存行的一个或两个元素之后再读取所有元素。(我用这些术语对问题的描述更新了您的问题。)
Hadi 的 SIMD 加载是使用一条指令获取所有数据的好主意。
据我们所知,_mm_load_si128()
它的 8 字节块实际上是原子的,因此它可以安全地替换.load(mo_relaxed)
原子的。但是请参阅矢量加载/存储和收集/分散的每元素原子性?- 对此没有明确的书面保证。
如果您使用过_mm256_loadu_si256()
,请注意 GCC 的默认调整-mavx256-split-unaligned-load
: 为什么 gcc 不将 _mm256_loadu_pd 解析为单个 vmovupd? 因此,除了需要避免缓存行拆分之外,这是使用对齐负载的另一个好理由。
但是我们是用 C 语言编写的,而不是 asm,所以我们需要担心std::atomic
with的其他一些事情mo_relaxed
:特别是从同一个地址重复加载可能不会给出相同的值。 您可能需要取消引用 avolatile __m256i*
来模拟什么load(mo_relaxed)
。
atomic_thread_fence()
如果您想要更强的排序,您可以使用;我认为在实践中支持英特尔内在函数的 C++11 编译器将订购 volatile 取消引用 wrt。栅栏的方式与std::atomic
加载/存储相同。在 ISO C++ 中,volatile
对象仍然受到数据竞争 UB 的影响,但在可以编译 Linux 内核的实际实现中,可以volatile
用于多线程。(Linux 使用volatile
和内联 asm 滚动它自己的原子,我认为这是 gcc/clang 支持的行为。)鉴于volatile
实际所做的(内存中的对象与 C++ 抽象机匹配),它基本上只是自动工作,尽管有任何规则-律师担心它在技术上是 UB。编译器无法知道或关心的是 UB,因为这就是volatile
.
在实践中,有充分的理由相信在 Haswell 及以后的整个对齐的 32 字节加载/存储是原子的。当然用于从 L1d 读取到乱序后端,甚至用于在内核之间传输缓存线。(例如,多套接字 K10 可以使用 HyperTransport 撕裂 8 字节边界,所以这确实是一个单独的问题)。利用它的唯一问题是缺乏任何书面保证或 CPU 供应商批准的方法来检测此“功能”。
除此之外,对于可移植代码,它可能有助于提升auto three = something.three;
分支;分支错误预测使核心有更多时间在第三次加载之前使行无效。
但是编译器可能不会尊重该源更改,并且仅在需要它的情况下加载它。但是无分支代码总是会加载它,所以也许我们应该鼓励它
bar(one, two, one == 0 ? something.three : 0);
Broadwell 可以在每个时钟周期运行 2 个负载(就像自 Sandybridge 和 K8 以来的所有主流 x86 一样);微指令通常以最旧的就绪优先顺序执行,因此很可能(如果此加载确实必须等待来自另一个内核的数据)我们的2 个加载微指令将在数据到达后可能的第一个周期中执行。
第三次加载 uop 有望在那之后的循环中运行,留下一个非常小的窗口让无效导致问题。
或者在每个时钟负载只有 1 个的 CPU 上,仍然在 asm 中相邻的所有 3 个负载会减少失效窗口。
但如果one == 0
很少见,则three
通常根本不需要,因此无条件加载会带来不必要的请求的风险。 因此,如果您不能用一个 SIMD 负载覆盖所有数据,则在调整时必须考虑这种权衡。
正如评论中所讨论的,软件预取可能有助于隐藏一些内核间延迟。
但是您必须比普通数组更晚地预取,因此在调用之前在代码中找到经常运行约 50 到约 100 个周期f1()
的位置是一个难题,并且可能会“感染”许多其他代码的细节与其正常运行无关。你需要一个指向正确缓存行的指针。
您需要 PF 足够晚,以使需求负载在预取数据实际到达之前发生几个(几十个)周期。这与正常用例相反,其中 L1d 是一个缓冲区,用于在需求负载到达之前预取并保存已完成预取的数据。但是您需要 load_hit_pre.sw_pf
perf 事件(负载命中预取),因为这意味着需求负载发生在数据仍在传输中时,然后才有可能失效。
这意味着调优比平时更加脆弱和困难,因为与早晚不会受到伤害的几乎平坦的预取距离最佳位置不同,早先隐藏了更多延迟,直到它允许失效为止,所以它是一个一直倾斜到悬崖。(并且任何过早的预取只会使整体争用变得更糟。)