Prefetch from MMIO?



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

如果可能的话,你知道预取的值存储在哪里吗?在我能够为它发出加载之前,什么可能导致它失效?例如,如果我为一些不相关的东西发出一个同步指令,比如sfence,这会导致预取的值失效吗?


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

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


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

我要感谢Peter Cordes, John D McCalpin, Neel Natu, Christian Ludloff和David mazi的帮助!

为了预取,您需要能够在CPU缓存层次结构中存储MMIO读取。当您使用UC或WC页表项时,您不能这样做。但是,如果使用WT页表项,则可以使用缓存层次结构。

唯一需要注意的是,当您使用WT页表项时,以前带有陈旧数据的MMIO读取可能会在缓存中停留。您必须在软件中实现一致性协议,以便从缓存中清除过时的缓存行,并通过MMIO读取读取最新的数据。这在我的情况下是可以的,因为我控制PCIe设备上发生的事情,所以我知道何时刷新。但是,您可能不知道在所有情况下何时冲洗,这可能使此方法对您没有帮助。

我是这样设置我的系统的:

  1. 将映射到PCIe BAR的页表项标记为WT。您可以使用ioremap_wt()(如果BAR已经映射到内核中,则使用ioremap_change_attr())。

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

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

  2. 你现在可以发出一个预取到一个地址在PCIe BAR和有预取缓存线存储在缓存层次结构。Linux有prefetch()函数来帮助预取。

  3. 必须在软件中实现一个简单的一致性协议,以确保由PCIe BAR支持的过时缓存线从缓存中刷新。您可以使用clflush来刷新过时的缓存线。Linux有clflush()函数来帮助完成这个任务。

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

  1. 请注意,在我的系统中,我在clflush之后立即发出预取。但是,下面的代码是错误的:
clflush(address);
prefetch(address);

这个代码是不正确的,因为根据https://c9x.me/x86/html/file_module_x86_id_252.html,预取可以在clflush之前重新排序。因此,预取可以在clflush之前发出,并且预取可能在clflush发生时无效。

要解决这个问题,根据链接,您应该在clflush和预取之间发出cpuid:

int eax, ebx, ecx, edx;
clflush(address);
cpuid(0, &eax, &ebx, &ecx, &edx);
prefetch(address);

Peter Cordes表示,发布lfence而不是上面的cpuid就足够了。

最新更新