从 MMIO 预取?

Jac*_*ies 5 memory x86 atomic prefetch pci-e

是否可以对 PCIe BAR 中的 MMIO 区域支持的地址(并通过 UC 或 WC 页表条目映射)发出预取?我目前正在对该地址发出负载,这会导致超线程停滞相当长一段时间。有一个非临时访问提示 via PREFETCHNTA,所以看起来这可能是可能的。

如果可能的话,您是否知道预取值存储在哪里以及在我能够为其发出加载之前什么可能导致它变得无效?例如,如果我针对sfence不相关的内容发出同步指令,这是否会导致预取值失效?


来自英特尔软件开发手册:

“来自不可缓存或 WC 内存的预取将被忽略......应该注意的是,处理器可以自由地推测性地从系统内存区域获取和缓存数据,这些区域被分配了允许推测性读取的内存类型(即 WB、WC和 WT 内存类型)。


MMIO 区域所在的 PCIe BAR 被标记为可预取,因此我不确定这是否意味着预取将与上面手册中的语言一起使用。

Jac*_*ies 3

我要感谢 Peter Cordes、John D McCalpin、Neel Natu、Christian Ludloff 和 David Mazi\xc3\xa8res 为解决这个问题提供的帮助!

\n

为了预取,您需要能够将 MMIO 读取存储在 CPU 缓存层次结构中。当您使用 UC 或 WC 页表条目时,不能执行此操作。但是,如果使用 WT 页表条目,则可以使用缓存层次结构。

\n

唯一需要注意的是,当您使用 WT 页表条目时,先前带有过时数据的 MMIO 读取可能会在缓存中徘徊。您必须在软件中实现一致性协议,以刷新缓存中的过时缓存行并通过 MMIO 读取来读取最新数据。这对我来说没问题,因为我控制 PCIe 设备上发生的事情,所以我知道何时刷新。不过,您可能不知道在所有情况下何时刷新,这可能会使这种方法对您没有帮助。

\n

以下是我设置系统的方法:

\n
    \n
  1. 将映射到 PCIe BAR 的页表条目标记为 WT。您可以用于ioremap_wt()此目的(或者ioremap_change_attr()如果 BAR 已映射到内核中)。

    \n
  2. \n
  3. 根据https://sandpile.org/x86/coherent.htm,PAT类型和MTRR类型之间存在冲突。PCIe BAR 的 MTRR 类型也必须设置为 WT,否则 PAT WT 类型将被忽略。您可以使用下面的命令来执行此操作。请务必使用 PCIe BAR 地址(可以通过 看到lspci -vv)和 PCIe BAR 大小更新命令。大小是以字节为单位的十六进制值。

    \n
  4. \n
\n
echo "base=$ADDRESS size=$SIZE type=write-through" >| /proc/mtrr\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 作为此时的快速检查,您可能希望在循环中向 BAR 中的同一缓存行发出大量 MMIO 读取。您应该会看到每次 MMIO 读取的成本在第一次 MMIO 读取后大幅下降。第一次 MMIO 读取仍然很昂贵,因为您需要从 PCIe 设备获取值,但后续读取应该便宜得多,因为它们都从缓存层次结构中读取。

    \n
  2. \n
  3. 现在,您可以向 PCIe BAR 中的地址发出预取,并将预取的缓存行存储在缓存层次结构中。Linux 具有prefetch()帮助发出预取的功能。

    \n
  4. \n
  5. 您必须在软件中实现简单的一致性协议,以确保 PCIe BAR 支持的过时缓存行从缓存中刷新。您可以使用它clflush来刷新陈旧的缓存行。Linux 有一个clflush()功能可以帮助解决这个问题。

    \n
  6. \n
\n

此场景中的注意事项clflush:由于内存类型为 WT,因此每个存储都会访问高速缓存中的高速缓存行和 MMIO。因此,从CPU的角度来看,高速缓存中高速缓存行的内容始终与MMIO的内容匹配。因此,clflush只会使缓存中的缓存行无效——它不会将过时的缓存行写入 MMIO。

\n
    \n
  1. 请注意,在我的系统中,我在clflush. 然而,下面的代码是不正确的:
  2. \n
\n
clflush(address);\nprefetch(address);\n
Run Code Online (Sandbox Code Playgroud)\n

此代码不正确,因为根据https://c9x.me/x86/html/file_module_x86_id_252.html,预取可能会在clflush. 因此,预取可以在 之前发出clflush,并且当 发生时预取可能会无效clflush

\n

要解决此问题,根据链接,您应该cpuidclflush和 预取之间发出:

\n
int eax, ebx, ecx, edx;\n\nclflush(address);\ncpuid(0, &eax, &ebx, &ecx, &edx);\nprefetch(address);\n
Run Code Online (Sandbox Code Playgroud)\n

Peter Cordes 表示,发出一个lfence替代cpuid上述内容就足够了。

\n