在 x86 上独占访问 L1 缓存行?
Exclusive access to L1 cacheline on x86?
如果有一个 64 字节的缓冲区read/written那么它很可能会保存在 L1 中;但是有什么办法可以强制这种行为吗?
例如,让一个内核独占访问这 64 个字节,并告诉它不要与其他内核或内存控制器同步数据,这样无论 CPU 认为它经常被使用。
不,x86 不允许您这样做。您可以使用 clfushopt
强制逐出,或者(在即将到来的 CPUs 上)只写回而不用 clwb
逐出,但您不能在缓存中固定一行或禁用一致性。
您可以将整个 CPU(或单个内核?)置于缓存即 RAM(又名无填充)模式以禁用与内存控制器的同步,并禁用永远写回数据. Cache-as-Ram (no fill mode) Executable Code. It's typically used by BIOS / firmware in early boot before configuring the memory controllers. It's not available on a per-line basis, and is almost certainly not practically useful here. Fun fact: leaving this mode is one of the use-cases for 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 中保持热度,L2 也是核心和私有的,并且在最近的设计中非常快(英特尔自 Nehalem 以来)。英特尔在除 Skylake-AVX512 之外的 CPU 上包含 L3,这意味着留在 L1d 也意味着留在 L3。
所有这一切都意味着,对于一个内核大量使用的线路,以任何一种频率都不太可能出现完全高速缓存未命中到 DRAM 的情况。所以吞吐量应该不是问题。 我猜你可能想要实时延迟,其中一次函数调用的最坏情况 运行 时间很重要。从代码其他部分的缓存行中虚拟读取可能有助于保持它的热度。
但是,如果来自 L3 缓存中其他内核的压力导致从 L3 中逐出该行,具有包容性 L3 的英特尔 CPU 也必须强制从仍然热的内部缓存中逐出。 IDK 是否有任何机制让 L3 知道一条线路在核心的 L1d 中被大量使用,因为这不会产生任何 L3 流量。
我不知道这在实际代码中是个大问题。 L3 是高度关联的(如 16 或 24 路),因此在你被逐出之前需要很多冲突。 L3 还使用了一个更复杂的索引函数(就像一个真正的散列函数,而不仅仅是通过取连续的位范围取模)。在 IvyBridge 及更高版本中,它还使用自适应替换策略来减少因接触大量不会经常重用的数据而导致的逐出。 http://blog.stuffedcow.net/2013/01/ivb-cache-replacement/.
另见
@AlexisWilke 指出 对于某些用例,您可以使用向量寄存器而不是缓存行。 . You could globally dedicate some vector regs to this purpose. To get this in gcc-generated code, maybe use -ffixed-ymm8
, or declare it as a volatile global register variable. (How to inform GCC to not use a particular register)
使用 ALU 指令或存储转发来获取数据 to/from 向量 reg 将为您提供有保证的延迟,而不会出现数据缓存丢失的可能性。但是代码缓存未命中仍然是极低延迟的问题。
没有直接的方法可以在 Intel 和 AMD x86 处理器上实现这一点,但您可以通过一些努力获得非常接近的结果。首先,您说您担心缓存行可能会从 L1 中被逐出,因为其他核心可能会访问它。这只会在以下情况下发生:
- 线路是共享的,因此可以被系统中的多个座席同时接入。如果另一个代理试图读取该行,其状态将从修改或独占更改为共享。也就是说,它将在 L1 中说明。另一方面,如果另一个代理试图写入该行,则必须从 L1 中使其无效。
- 线程可以是私有的或共享的,但是线程被 OS 重新安排到另一个核心上的 运行。与前面的情况类似,如果它尝试读取该行,它的状态将在两个 L1 缓存中从修改或独占更改为共享。如果它试图写入该行,则必须从它所在的前一个内核的 L1 中使其无效 运行ning。
还有其他原因导致线路可能会从 L1 中逐出,我将在稍后讨论。
如果线路是共享的,则不能禁用一致性。但是,您可以做的是制作它的私有副本,这实际上会禁用一致性。如果这样做可能会导致错误行为,那么您唯一可以做的就是在超线程 (SMT) Intel 处理器的同一物理内核上将共享该行的所有线程的亲和力设置为 运行。由于L1在逻辑核心之间共享,线路不会因为共享而被逐出,但它仍然可以由于其他原因而被逐出。
设置线程的亲和力并不能保证其他线程无法在同一核心上调度到 运行。为了减少在同一个核心上调度其他线程(不访问线路)或将线程重新调度到其他物理核心上的运行的可能性,您可以提高线程(或所有线程的优先级)分享线路)。
Intel 处理器大多是 2 路超线程,因此您一次只能 运行 两个共享线路的线程。所以如果你玩弄线程的亲和力和优先级,性能会以有趣的方式改变。你必须测量它。最近的 AMD 处理器也支持 SMT。
如果线路是私有的(只有一个线程可以访问它),英特尔处理器中同级逻辑核心上的线程 运行ning 可能会导致线路被逐出,因为 L1 是竞争共享的,取决于它的内存访问行为。我将很快讨论如何处理这个问题。
另一个问题是中断和异常。在 Linux 和其他 OSes 上,您可以配置哪些内核应该处理哪些中断。我认为将所有中断映射到所有其他内核是可以的,除了周期性定时器中断,其中断处理程序的行为是 OS 依赖的并且使用它可能不安全。根据您想为此花费多少精力,您可以执行精心设计的实验来确定定时器中断处理程序对 L1D 缓存内容的影响。你也应该避免例外。
我能想到一行可能无效的两个原因:
- 一个(可能是推测性的)RFO,意图从另一个核心进行修改。
- 选择要驱逐的行以使另一行成为 space。这取决于缓存层次结构的设计:
- L1 缓存放置策略。
- 一级缓存替换策略。
- 下级缓存是否包含。
replacement policy一般是不可配置的,所以要尽量避免冲突L1 misses,这取决于placement policy,这取决于微架构。在 Intel 处理器上,L1D 通常同时进行虚拟索引和物理索引,因为用于索引的位不需要转换。由于您知道所有内存访问的虚拟地址,因此您可以确定将从哪个缓存集分配哪些行。您需要确保映射到同一集合的行数(包括您不希望它被逐出的行)不超过缓存的关联性。否则,您将受到更换政策的支配。另请注意,L1D 预取器也可以更改缓存的内容。您可以在 Intel 处理器上禁用它并测量它在这两种情况下的影响。我想不出一种简单的方法来处理包容性较低级别的缓存。
我认为 "pinning" 缓存中一行的想法很有趣并且很有用。它是高速缓存和暂存器之间的混合体。该行就像一个映射到虚拟地址 space.
的临时寄存器
这里的主要问题是您希望同时 读取和写入该行,同时仍将其保存在缓存中。目前不支持这种行为。
如果有一个 64 字节的缓冲区read/written那么它很可能会保存在 L1 中;但是有什么办法可以强制这种行为吗?
例如,让一个内核独占访问这 64 个字节,并告诉它不要与其他内核或内存控制器同步数据,这样无论 CPU 认为它经常被使用。
不,x86 不允许您这样做。您可以使用 clfushopt
强制逐出,或者(在即将到来的 CPUs 上)只写回而不用 clwb
逐出,但您不能在缓存中固定一行或禁用一致性。
您可以将整个 CPU(或单个内核?)置于缓存即 RAM(又名无填充)模式以禁用与内存控制器的同步,并禁用永远写回数据. Cache-as-Ram (no fill mode) Executable Code. It's typically used by BIOS / firmware in early boot before configuring the memory controllers. It's not available on a per-line basis, and is almost certainly not practically useful here. Fun fact: leaving this mode is one of the use-cases for 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 中保持热度,L2 也是核心和私有的,并且在最近的设计中非常快(英特尔自 Nehalem 以来)。英特尔在除 Skylake-AVX512 之外的 CPU 上包含 L3,这意味着留在 L1d 也意味着留在 L3。
所有这一切都意味着,对于一个内核大量使用的线路,以任何一种频率都不太可能出现完全高速缓存未命中到 DRAM 的情况。所以吞吐量应该不是问题。 我猜你可能想要实时延迟,其中一次函数调用的最坏情况 运行 时间很重要。从代码其他部分的缓存行中虚拟读取可能有助于保持它的热度。
但是,如果来自 L3 缓存中其他内核的压力导致从 L3 中逐出该行,具有包容性 L3 的英特尔 CPU 也必须强制从仍然热的内部缓存中逐出。 IDK 是否有任何机制让 L3 知道一条线路在核心的 L1d 中被大量使用,因为这不会产生任何 L3 流量。
我不知道这在实际代码中是个大问题。 L3 是高度关联的(如 16 或 24 路),因此在你被逐出之前需要很多冲突。 L3 还使用了一个更复杂的索引函数(就像一个真正的散列函数,而不仅仅是通过取连续的位范围取模)。在 IvyBridge 及更高版本中,它还使用自适应替换策略来减少因接触大量不会经常重用的数据而导致的逐出。 http://blog.stuffedcow.net/2013/01/ivb-cache-replacement/.
另见
@AlexisWilke 指出 对于某些用例,您可以使用向量寄存器而不是缓存行。 -ffixed-ymm8
, or declare it as a volatile global register variable. (How to inform GCC to not use a particular register)
使用 ALU 指令或存储转发来获取数据 to/from 向量 reg 将为您提供有保证的延迟,而不会出现数据缓存丢失的可能性。但是代码缓存未命中仍然是极低延迟的问题。
没有直接的方法可以在 Intel 和 AMD x86 处理器上实现这一点,但您可以通过一些努力获得非常接近的结果。首先,您说您担心缓存行可能会从 L1 中被逐出,因为其他核心可能会访问它。这只会在以下情况下发生:
- 线路是共享的,因此可以被系统中的多个座席同时接入。如果另一个代理试图读取该行,其状态将从修改或独占更改为共享。也就是说,它将在 L1 中说明。另一方面,如果另一个代理试图写入该行,则必须从 L1 中使其无效。
- 线程可以是私有的或共享的,但是线程被 OS 重新安排到另一个核心上的 运行。与前面的情况类似,如果它尝试读取该行,它的状态将在两个 L1 缓存中从修改或独占更改为共享。如果它试图写入该行,则必须从它所在的前一个内核的 L1 中使其无效 运行ning。
还有其他原因导致线路可能会从 L1 中逐出,我将在稍后讨论。
如果线路是共享的,则不能禁用一致性。但是,您可以做的是制作它的私有副本,这实际上会禁用一致性。如果这样做可能会导致错误行为,那么您唯一可以做的就是在超线程 (SMT) Intel 处理器的同一物理内核上将共享该行的所有线程的亲和力设置为 运行。由于L1在逻辑核心之间共享,线路不会因为共享而被逐出,但它仍然可以由于其他原因而被逐出。
设置线程的亲和力并不能保证其他线程无法在同一核心上调度到 运行。为了减少在同一个核心上调度其他线程(不访问线路)或将线程重新调度到其他物理核心上的运行的可能性,您可以提高线程(或所有线程的优先级)分享线路)。
Intel 处理器大多是 2 路超线程,因此您一次只能 运行 两个共享线路的线程。所以如果你玩弄线程的亲和力和优先级,性能会以有趣的方式改变。你必须测量它。最近的 AMD 处理器也支持 SMT。
如果线路是私有的(只有一个线程可以访问它),英特尔处理器中同级逻辑核心上的线程 运行ning 可能会导致线路被逐出,因为 L1 是竞争共享的,取决于它的内存访问行为。我将很快讨论如何处理这个问题。
另一个问题是中断和异常。在 Linux 和其他 OSes 上,您可以配置哪些内核应该处理哪些中断。我认为将所有中断映射到所有其他内核是可以的,除了周期性定时器中断,其中断处理程序的行为是 OS 依赖的并且使用它可能不安全。根据您想为此花费多少精力,您可以执行精心设计的实验来确定定时器中断处理程序对 L1D 缓存内容的影响。你也应该避免例外。
我能想到一行可能无效的两个原因:
- 一个(可能是推测性的)RFO,意图从另一个核心进行修改。
- 选择要驱逐的行以使另一行成为 space。这取决于缓存层次结构的设计:
- L1 缓存放置策略。
- 一级缓存替换策略。
- 下级缓存是否包含。
replacement policy一般是不可配置的,所以要尽量避免冲突L1 misses,这取决于placement policy,这取决于微架构。在 Intel 处理器上,L1D 通常同时进行虚拟索引和物理索引,因为用于索引的位不需要转换。由于您知道所有内存访问的虚拟地址,因此您可以确定将从哪个缓存集分配哪些行。您需要确保映射到同一集合的行数(包括您不希望它被逐出的行)不超过缓存的关联性。否则,您将受到更换政策的支配。另请注意,L1D 预取器也可以更改缓存的内容。您可以在 Intel 处理器上禁用它并测量它在这两种情况下的影响。我想不出一种简单的方法来处理包容性较低级别的缓存。
我认为 "pinning" 缓存中一行的想法很有趣并且很有用。它是高速缓存和暂存器之间的混合体。该行就像一个映射到虚拟地址 space.
的临时寄存器这里的主要问题是您希望同时 读取和写入该行,同时仍将其保存在缓存中。目前不支持这种行为。