CPU缓存抑制

CPU cache inhibition

假设我有事实上的标准 x86 CPU,具有 3 级缓存,L1/L2 私有,以及内核之间共享的 L3。有没有办法分配共享内存,其数据不会缓存在 L1/L2 私有缓存中,而是只会缓存在 L3 中?我不想从内存中获取数据(这太昂贵了),但我想在将共享数据放入私有缓存和不将共享数据放入私有缓存的情况下进行性能试验。

假设 L3 在核心之间共享(大概是物理索引缓存),因此不会导致任何错误共享或缓存行失效对于大量使用的共享数据。

任何解决方案(如果存在)都必须以编程方式完成,使用基于英特尔的 CPUs 的 C and/or 程序集(相对现代的 Xeon 架构(skylake、broadwell),运行宁linux基于OS.

编辑:

我有延迟敏感代码,它使用一种共享内存形式进行同步。数据将在 L3 中,但在读取或写入时将进入 L1/L2,具体取决于缓存包容性策略。 根据该问题的暗示,数据将不得不无效,从而增加不必要的(我认为)性能损失。我想看看是否可以仅通过某些页面策略或仅在 L3 中的特殊说明来存储数据。

我知道出于安全原因可以使用特殊内存寄存器来禁止缓存,但这需要 CPL0 权限。

编辑2:

我在高性能系统上处理 运行 的并行代码,有时几个月。这些系统是高核心数系统(例如 40-160+ 核心),定期执行需要在 usecs 中执行的同步。

我相信你不应该(也可能不能)关心,希望共享内存在L3。顺便说一句,user-space C code runs in virtual address space and your other cores might (and often do) run some other unrelated process.

硬件和MMU(由内核配置)将确保L3被正确共享。

but I'd like to experiment with performance with and without bringing the shared data into private caches.

据我了解(相当不了解)最近的英特尔硬件,这是不可能的(至少在用户领域是不可能的)。

也许您可以考虑 PREFETCH 机器指令和 __builtin_prefetch GCC 内置指令(这与您想要的相反,它将数据带到更近的缓存中)。参见 this and that

顺便说一句,内核 preemptive scheduling, so context switches can happen at any moment (often several hundred times each second). When (at context switch time) another process is scheduled on the same core, the MMU needs to be reconfigured (because each process has its own virtual address space,缓存又是 "cold")。

您可能对 processor affinity. See sched_setaffinity(2). Read about about Real-Time Linux. See sched(7). And see numa(7).

感兴趣

我完全不确定您担心的性能影响是否明显(而且我相信这在用户中是不可避免的-space)。

也许您可能会考虑将您的敏感代码移至内核 space(因此具有 CPL0 特权),但这可能需要数月的工作并且可能不值得付出努力。我什至不会尝试。

您是否考虑过对延迟敏感代码采用其他完全不同的方法(例如,在 OpenCL 中为您的 GPGPU 重写它)?

x86 无法通过 L1D/L2 而不是 L3 进行绕过或写入的存储。有绕过所有缓存的 NT 商店。 强制 写回 L3 的任何内容也会强制一直写回内存。 (例如 clwb 指令)。这些专为非易失性 RAM 用例或非相干 DMA 而设计,在这些情况下,将数据提交到实际 RAM 很重要。

也没有办法进行绕过 L1D 的加载(除了使用 SSE4.1 movntdqa 的 USWC 内存,但它不是 "special" 其他内存类型)。 prefetchNTA可以绕过L2,根据Intel的优化手册。

在执行读取操作的核心上预取应该有助于触发从其他核心写回到 L3,并传输到您自己的 L1D。但这只有在您要加载之前准备好地址时才有用。 (几十个循环才有用。)

Intel CPU 使用共享的包容性 L3 缓存作为片上缓存一致性的后盾。 2-socket 必须监听另一个 socket,但是支持超过 2P 的 Xeons 有监听过滤器来跟踪移动的缓存行。

当你读到另一个核心最近写的一行时,它在你的 L1D 中总是无效的。 L3 是包含标签的,它的标签有额外的信息来跟踪哪个内核有线路。 (即使该行在某处的 L1D 中处于 M 状态也是如此,这要求它在 L3 中无效,according to normal MESI。)因此,在您的缓存未命中检查 L3 标记后,它会触发对L1 具有将其写回 L3 缓存的行(并且可能将其直接发送到内核而不是想要它)。

Skylake-X (Skylake-AVX512) 没有包容性的 L3(它有一个更大的私有 L2 和一个更小的 L3),但它仍然有一个标签包容性结构来跟踪哪个核心有一条线。它还使用网状结构而不是环状结构,L3 延迟似乎比 Broadwell 差很多。


可能有用:使用直写缓存策略映射共享内存区域的延迟关键部分。 IDK 如果这个补丁曾经进入主线 Linux 内核,但请参阅 this patch from HP: Support Write-Through mapping on x86。 (正常政策是WB。)

还相关:主内存和高速缓存性能 Intel Sandy Bridge 和 AMD Bulldozer,深入了解 2 插槽 SnB 上的延迟和带宽,用于不同起始状态的缓存行。

有关 Intel CPU 内存带宽的更多信息,请参阅 ,尤其是延迟限制平台部分。 (只有 10 个 LFB 限制了单核带宽)。


相关: 有一些实验结果让一个线程垃圾邮件写入一个位置,而另一个线程读取它。

请注意,缓存未命中本身并不是唯一的影响。您还会从执行负载的核心中的错误推测中得到很多 machine_clears.memory_ordering。 (x86 的内存模型是强有序的,但真实的 CPU 推测性地提前加载并在极少数情况下中止,即缓存行在加载应该具有 "happened".

之前变得无效

您不会找到禁用 Intel CPU L1 或 L2 的好方法:事实上,除了一些特定场景,例如 Peter 中涵盖的 UC 内存区域(这会降低您的性能因为他们也不使用 L3),特别是 L1 从根本上涉及读写。

但是,您可以做的是使用 L1 和 L2 定义明确的缓存行为来强制逐出您只想保存在 L3 中的数据。在最近的 Intel 架构上,L1 和 L2 都表现为伪 LRU "standard associative" 缓存。 "standard associative" 我的意思是您在维基百科或硬件 101 course 中阅读的缓存结构,其中缓存分为 2^N 个集合,其中有 M 个条目(对于 M-way associative cache) 和 N 来自地址的连续位用于查找集合。

这意味着您可以准确预测哪些缓存行最终会出现在同一组中。例如,Skylake 有一个 8 路 32K L1D 和一个 4 路 256K L2。这意味着相隔 64K 的高速缓存行将落入 L1 和 L2 上的同一组。通常,将大量使用的值放入同一缓存行是一个问题(缓存集争用可能会使您的缓存看起来比实际小得多)-但在这里您可以利用它来发挥自己的优势!

当你想从 L1 和 L2 中逐出一行时,只需将 8 个或更多值读取或写入距离目标行 64K 的其他行。根据基准测试(或底层应用程序)的结构,您甚至可能不需要虚拟写入:在您的内部循环中,您可以简单地使用 16 个值,所有值都间隔 64K 而不是 return 到第一个值,直到你已经访问了其他 15 个。这样,每一行都会 "naturally" 在你使用它之前被驱逐。

请注意,每个核心上的虚拟写入不必相同:每个核心都可以写入 "private" 虚拟行,因此您不会为虚拟写入添加争用。

一些并发症:

  • 我们在这里讨论的地址(当我们说“距离目标地址 64K”时)是 物理 地址。如果您使用的是 4K 页面,则可以通过以 4K 的偏移量写入来从 L1 中逐出,但要使其适用于 L2,您需要 64K physical 偏移量 - 但您无法获得这是可靠的,因为每次您跨越 4K 页面边界时,您都在写入某个任意物理页面。您可以通过确保为所涉及的缓存行使用 2MB 大页面来解决此问题。
  • 我说“8 或更多”高速缓存行需要 read/written。那是因为缓存很可能使用某种伪 LRU 而不是精确的 LRU。您必须进行测试:您可能会发现伪 LRU 与您正在使用的模式的精确 LRU 一样工作,或者您可能会发现您需要超过 8 次写入才能可靠地逐出。

一些其他注意事项:

  • 您可以使用 perf 公开的性能计数器来确定您在 L1 vs L2 vs L3 中实际击中的频率,以确保您的技巧有效。
  • L3 通常不是 "standard associative cache":而是通过散列比典型缓存更多的地址位来查看该集合。散列意味着您最终不会只使用 L3 中的几行:您的目标行和虚拟行应该很好地分布在 L3 周围。如果您发现您使用的是未散列的 L3,它应该仍然有效(因为 L3 更大,您仍然会在缓存集中分散) - 但您也必须更加小心 L3 中可能的驱逐。

英特尔最近宣布了一项似乎与该问题相关的新指令。该指令称为 CLDEMOTE。它将数据从较高级别的缓存移动到较低级别的缓存。 (可能从 L1 或 L2 到 L3,尽管规范在细节上并不准确。)"This may accelerate subsequent accesses to the line by other cores ...."

https://software.intel.com/sites/default/files/managed/c5/15/architecture-instruction-set-extensions-programming-reference.pdf