【问题标题】:Exclusive access to L1 cacheline on x86?独占访问 x86 上的 L1 缓存线?
【发布时间】:2019-01-05 12:17:45
【问题描述】:

如果一个 64 字节的缓冲区被大量读取/写入,那么它很可能会保存在 L1 中;但是有没有办法强迫这种行为?

例如,让一个内核独占访问这 64 个字节,并告诉它不要与其他内核或内存控制器同步数据,以便这些 64 个字节始终存在于一个内核的 L1 中,无论 CPU 是否认为它是经常使用。

【问题讨论】:

  • 在我知道的任何 x86 机器上都没有。也没有办法关闭缓存一致性,并且之前的尝试已被证明是quite destructive。同样,也无法手动控制哪些数据缓存在哪里。
  • 如果您只需要快速 64 字节,4 x XMM 寄存器会为您保存...假设 99% 的时间它可能会更快更容易地编写从内存读取的代码无论如何都会在 L1 缓存中。

标签: performance assembly x86 cpu-cache low-level


【解决方案1】:

不,x86 不允许您这样做。您可以使用 clfushopt 强制驱逐,或者(在即将推出的 CPU 上)仅使用 clwb 进行回写而不驱逐,但您不能将行固定在缓存中或禁用一致性。


您可以将整个 CPU(或单个内核?)置于缓存即 RAM(也称为无填充)模式,以禁用与内存控制器的同步,并禁止回写数据。 Cache-as-Ram (no fill mode) Executable Code。它通常由 BIOS / 固件在配置内存控制器之前的早期启动中使用。它不是按行提供的,而且几乎可以肯定在这里实际上没有用。有趣的事实:离开此模式是 invd 的用例之一,它在不写回的情况下删除缓存数据,而不是 wbinvd

我不确定无填充模式是否会阻止从 L1d 驱逐到 L3 或其他什么;或者如果数据只是在驱逐时丢弃。因此,您只需避免访问 7 个以上的其他缓存行,这些缓存行是您在 L1d 中关心的缓存行的别名,或 L2/L3 的等效缓存行。


如果能够强制一个内核无限期地挂在 L1d 行上,而不响应 MESI 将其写回/共享的请求,这将使其他内核在触及该行时容易被锁定。所以很明显,如果存在这样的功能,它将需要内核模式。 (并且使用硬件虚拟化,需要管理程序权限。)它还可以阻止硬件 DMA(因为现代 x86 具有缓存一致的 DMA)。

因此,支持这样的功能需要 CPU 的许多部分来处理无限期延迟,目前可能存在一些上限,如果存在这样的情况,它可能比 PCIe 超时更短。 (我不编写驱动程序或构建真正的硬件,只是猜测)。

正如@fuz 所指出的,违反一致性的指令 (xdcbt) 是 tried on PowerPC (in the Xbox 360 CPU),错误推测的指令执行会导致灾难性的后果。所以实现起来困难


你通常不需要这个。

如果线路经常使用,更换 LRU 会使其保持热度。如果它以足够频繁的间隔从 L1d 丢失,那么它可能会在 L2 中保持热状态,这也是核心和私有的,并且在最近的设计中非常快(自 Nehalem 以来的英特尔)。英特尔在 Skylake-AVX512 以外的 CPU 上的包容性 L3 意味着留在 L1d 也意味着留在 L3。

所有这一切都意味着,对于一个内核大量使用的线路,无论以何种频率,都不太可能出现完全缓存未命中到 DRAM 的情况。所以吞吐量应该不是问题。 我想您可能希望将其用于实时延迟,在这种情况下,一次函数调用的最坏情况运行时间很重要。在代码的其他部分从缓存行进行虚拟读取可能有助于保持其热度。

但是,如果来自 L3 缓存中其他内核的压力导致该行从 L3 中逐出,则具有包容性 L3 的 Intel CPU 也必须强制从仍然热的内部缓存中逐出。 IDK 如果有任何机制让 L3 知道某条线路在核心的 L1d 中被大量使用,因为这不会产生任何 L3 流量。

我不知道这在实际代码中是一个很大的问题。 L3 具有高度关联性(如 16 或 24 路),因此在您被驱逐之前需要很多冲突。 L3 还使用了一个更复杂的索引函数(就像一个真正的散列函数,而不仅仅是通过取一个连续的位范围来取模)。在 IvyBridge 及更高版本中,它还使用自适应替换策略来减少因接触大量不会经常重用的数据而被驱逐。 http://blog.stuffedcow.net/2013/01/ivb-cache-replacement/.

另见Which cache mapping technique is used in intel core i7 processor?


@AlexisWilke 指出 对于某些用例,您可以使用向量寄存器而不是缓存行。 Using ymm registers as a "memory-like" storage location。您可以为此目的在全球范围内专门使用一些向量 reg。要在 gcc 生成的代码中获取此信息,可以使用 -ffixed-ymm8,或者将其声明为 volatile 全局寄存器变量。 (How to inform GCC to not use a particular register)

使用 ALU 指令或存储转发从向量 reg 获取数据或从向量 reg 获取数据将为您提供有保证的延迟,而不会出现数据缓存未命中的可能性。但是对于极低的延迟,代码缓存未命中仍然是一个问题。

【讨论】:

  • 你说得对,经常访问的线路不太可能被驱逐。但是,正如我在回答中所讨论的,线程调度、SMT、中断之类的东西仍然可以让线路被驱逐。我不知道为什么 OP 要这样做。但我认为从技术角度来看这个问题很有趣。我不确定在这种情况下“Cache-as-Ram”有多少用处。我以前没听说过。
  • 这是来自 Intel 的相对较新的patent,它涉及由多个缓存级别共享的 LRU 策略。我还找到了其他专利和研究论文。
  • @HadiBrais: no-fill 模式几乎可以肯定没有在这里有用(因为它不是每行的东西),但它是做奇怪事情的少数方法之一在 x86 上使用缓存。我在更新中添加了更多关于它的内容。
  • 不幸的是,我找不到任何文章说明在 Haswell 或更高版本的处理器中的任何缓存级别使用了哪些缓存替换策略。 paper 在第 5 页中说 Haswell 和 Skylake 使用与 Ivy Bridge 相同的策略,但他们引用了 2007 年的一篇论文。所以我不认为作者对此很确定。
  • @PeterCordes 你好,你提到 clwb 是缓存行的回写而不被驱逐。这是否经过某种测试?在很多文章中,他们都说同样的话,clwb 在刷新后不会驱逐缓存行,但英特尔文档说:硬件可以选择在缓存层次结构中的任何级别保留该行,并且在某些情况下,可能会使该行无效从缓存层次结构。我以某种方式对其进行了一些测试,在我看来,它在刷新后驱逐了所有缓存行,所以现在我想知道当它不驱逐它们时是什么情况。
【解决方案2】:

没有直接的方法可以在 Intel 和 AMD x86 处理器上实现这一目标,但您可以通过一些努力来实现这一目标。首先,您说您担心缓存行可能会从 L1 中逐出,因为其他一些内核可能会访问它。这只会在以下情况下发生:

  • 线路是共享的,因此系统中的多个代理可以同时访问它。如果另一个代理尝试读取该行,其状态将从已修改或独占更改为共享。也就是说,它将在 L1 中声明。另一方面,如果另一个代理尝试写入该行,则它必须从 L1 中失效。
  • 线程可以是私有的或共享的,但是线程被操作系统重新调度以在另一个内核上运行。与前一种情况类似,如果它尝试读取该行,它的状态将在两个 L1 缓存中从 Modified 或 Exclusive 变为 Shared。如果它试图写入该行,它必须从它运行的前一个内核的 L1 中失效。

还有其他原因会导致线路从 L1 中被逐出,我稍后会讨论。

如果线路是共享的,则不能禁用一致性。但是,您可以做的是制作它的私有副本,这实际上会禁用一致性。如果这样做可能会导致错误行为,那么您唯一能做的就是将共享该线路的所有线程的亲和性设置为在超线程 (SMT) 英特尔处理器上的同一物理内核上运行。由于 L1 是在逻辑核之间共享的,所以线路不会因为共享而被驱逐,但它仍然可以由于其他原因而被驱逐。

设置线程的亲和性并不能保证其他线程不能被安排在同一个内核上运行。为了减少在同一核心上调度其他线程(不访问该行)或重新调度线程以在其他物理核心上运行的概率,您可以提高线程(或共享该行的所有线程)的优先级.

英特尔处理器大多是 2 路超线程,因此您一次只能运行两个共享线路的线程。因此,如果您使用线程的亲和性和优先级,性能会以有趣的方式发生变化。你必须测量它。最近的 AMD 处理器也支持 SMT。

如果线路是私有的(只有一个线程可以访问它),在 Intel 处理器的同级逻辑内核上运行的线程可能会导致线路被逐出,因为 L1 是竞争共享的,具体取决于其内存访问行为。稍后我将讨论如何处理。

另一个问题是中断和异常。在 Linux 和其他操作系统上,您可以配置哪些内核应该处理哪些中断。我认为将所有中断映射到所有其他内核是可以的,除了周期性定时器中断,其中断处理程序的行为取决于操作系统,使用它可能不安全。根据您希望为此付出多少努力,您可以执行精心设计的实验,以确定定时器中断处理程序对 L1D 缓存内容的影响。你也应该避免例外。

我可以想到一行可能会失效的两个原因:

  • 一个(可能是推测性的)RFO,意图从另一个内核进行修改。
  • 选择要驱逐的线路为另一线路腾出空间。这取决于缓存层次结构的设计:
    • L1 缓存放置策略。
    • L1 缓存替换策略。
    • 较低级别的缓存是否包含在内。

替换策略通常是不可配置的,因此您应该努力避免冲突 L1 未命中,这取决于放置策略,这取决于微架构。在 Intel 处理器上,L1D 通常同时进行虚拟索引和物理索引,因为用于索引的位不需要转换。由于您知道所有内存访问的虚拟地址,因此您可以确定从哪个缓存集中分配哪些行。您需要确保映射到同一集合的行数(包括您不希望它被驱逐的行)不超过缓存的关联性。否则,您将受到更换政策的摆布。另请注意,L1D 预取器也可以更改缓存的内容。您可以在 Intel 处理器上禁用它并衡量它在这两种情况下的影响。我想不出一种简单的方法来处理包容性较低级别的缓存。

我认为在缓存中“固定”一行的想法很有趣并且很有用。它是高速缓存和便笺式存储器之间的混合体。该行就像一个映射到虚拟地址空间的临时寄存器。

这里的主要问题是您希望同时读取和写入该行,同时仍将其保留在缓存中。目前不支持这种行为。

【讨论】:

  • 借助英特尔的包容性 L3,L3 中的冲突驱逐可能会强制 L1d 中的驱逐。我不确定 L3 是否/如何跟踪 LRU / MRU 以避免驱逐在私有 L1d 中非常热的线路,并且永远不会从该核心为该线路生成任何 L3 流量。这是包容性缓存的一个缺点,也是 L3 必须高度关联的另一个原因。 (自 IvB 以来,L3 有一个自适应替换策略,以帮助减少因接触大量未重用的数据而被驱逐:blog.stuffedcow.net/2013/01/ivb-cache-replacement,但 IDK 是否可以帮助解决此问题。)
  • @PeterCordes 好点。尽管 L2 与 L1 一样是私有的,但它具有不同的放置策略(不同的组织和物理索引),因此包容性 L2 也可能由于 L2 而不是 L1 中的冲突而在 L1 中强制驱逐。
  • L2 是 NINE,它是自 Nehalem 以来英特尔包含的共享 L3。因此,驱逐可能是由其他核心的压力触发的。
猜你喜欢
  • 2011-01-27
  • 2012-05-03
  • 2010-10-27
  • 1970-01-01
  • 2012-07-02
  • 1970-01-01
  • 1970-01-01
  • 2012-06-04
  • 1970-01-01
相关资源
最近更新 更多