不幸的是,在 x86(或我所知道的任何其他 ISA)上,没有任何指令可以仅查询 TLB 或当前页表,并将结果存储在寄存器中。也许应该有,因为它可以很便宜地实现。
(为了查询虚拟内存中页面是否被分页,Linux 系统调用mincore(2) 会为开始的一系列页面生成存在/不存在的位图(给出为void* start / size_t length。这可能类似于硬件页表,因此可能可以让您避免页面错误,直到您触及内存,但与 TLB 或缓存无关。也许不排除 soft 页面错误,仅很难。当然这只是目前的情况:页面可能会在查询和访问之间被驱逐。)
这样的 CPU 功能会有用吗?在少数情况下可能是的
这样的东西很难以有回报的方式使用,因为每次“错误”尝试都是 CPU 时间/指令,没有完成任何有用的工作。但是,当您不关心遍历树/图的顺序时,像这样的情况可能会成功,并且某些节点可能在缓存、TLB 甚至只是 RAM 中很热,而其他节点则很冷甚至被分页到磁盘。
当内存紧张时,触摸一个冷页面甚至可以在你到达之前驱逐一个当前热页面。
普通 CPU(如现代 x86)可以进行推测性/无序页面遍历(以填充 TLB 条目),并且绝对可以推测性加载到缓存中,但不会出现页面错误。页面错误由内核在软件中处理。发生页面错误不能投机,并且正在序列化。 (CPU 不会重命名权限级别。)
所以软件预取可以廉价地让硬件在你接触其他内存时填充 TLB 和缓存,如果你要接触的第二个内存是冷的。如果它很热,而你先触摸冷的一面,那是不幸的。如果有一种便宜的方法来检查热/冷,那么当一个指针是热的而另一个是冷的时,它可能值得使用它以遍历顺序始终以正确的方式(至少在第一步)。除非只读事务非常便宜,否则实际上可能不值得使用 Margaret 的聪明答案。
如果您有 2 个指针,您最终将取消引用,其中一个指向已被分页的页面,而另一个是热的,最好的情况是以某种方式检测到这一点并让操作系统在一个页面中开始分页当您遍历已经在 RAM 中的一侧时,从后台的磁盘中获取。 (例如,使用 Windows
PrefetchVirtualMemory 或 Linux madvise(MADV_WILLNEED)。查看 OP 其他问题的答案:Minimizing page faults (and TLB faults) while "walking" a large graph)
这将需要系统调用,但系统调用昂贵并且会污染缓存 + TLB,尤其是在当前 x86 上,其中 Spectre + Meltdown 缓解增加了数千个时钟周期。 因此,为树中的每一对指针中的一个进行 VM 预取系统调用是不值得的。当所有指针都在 RAM 中时,您的速度会大大降低。
CPU 设计可能性
就像我说的,我认为当前的任何 ISA 都没有此功能,但我认为在硬件中支持运行类似于加载指令的指令会很容易,但会根据 TLB 查找而不是生成结果从 L1d 缓存中获取数据。
我想到了几种可能性:
-
queryTLB m8 指令根据内存操作数当前在 TLB(包括 2 级 TLB)中是否为热来写入标志(例如,CF=1 表示当前),从不进行页面遍历。 querypage m8 将在 TLB 未命中时执行页面遍历,并根据是否存在页表条目设置标志。将结果放入您可以测试/jcc 的 r32 整数寄存器中也是一种选择。
-
try_load r32, r/m32 指令在可能的情况下执行正常加载,但如果页面遍历未找到虚拟地址的有效条目,则设置标志而不是页面错误。 (例如,CF=1 表示有效,CF=0 表示整数结果 = 0 的中止,例如 rdrand。它可以使自己有用并根据值设置其他标志(SF/ZF/PF),如果有的话。 )
query 的想法只对性能有用,而不是正确性,因为在查询和使用之间总是存在间隙,在此期间页面可能会被取消映射。 (类似于IsBadXxxPtr Windows 系统调用,只不过它可能检查的是逻辑内存映射,而不是硬件页表。)
try_load insn 也设置/清除标志而不是提高 #PF 可以避免竞争条件。您可能有不同的版本,或者可能需要立即选择中止条件(例如,没有尝试页面遍历的 TLB 未命中)。
这些指令可以很容易地解码为一个加载微指令,可能只有一个。现代 x86 上的加载端口已经支持正常加载、软件预取、广播加载、零或符号扩展加载(movsx r32, m8 是英特尔上加载端口的单个 uop),甚至vmovddup ymm, m256(两个通道内广播) 出于某种原因,因此添加另一种加载 uop 似乎不是问题。
命中他们无权访问的 TLB 条目(仅内核映射)的负载目前在某些 x86 uarches (那些不易受 Meltdown 攻击的)上表现得特别。请参阅 Henry Wong 的血液 (stuffedcow.net) 上的 The Microarchitecture Behind Meltdown。根据他的测试,一些 CPU 会在 TLB/页面未命中(条目不存在)后为推测性执行后面的指令生成零。所以我们已经知道,用 TLB 命中/未命中结果做某事应该能够影响加载的整数结果。 (当然,TLB 未命中与特权条目命中不同。)
从负载设置标志在 x86 上通常不会发生(仅来自微融合负载+alu),所以如果英特尔曾经实现过这个想法,它可能也会用 ALU uop 实现。
但是,在 TLB/页面未命中或 L1d 未命中之外的条件下中止将需要外部缓存级别也支持此特殊请求。如果命中 L3 缓存但在 L3 未命中时中止时运行的 try_load 将需要 L3 缓存的支持。不过,我认为我们可以不这样做。
这种 CPU 架构理念的唾手可得的成果是减少页面错误和可能的页面遍历,这比 L3 缓存未命中要昂贵得多。
我怀疑尝试在 L3 缓存未命中上进行分支会使您在分支未命中方面付出太多代价,因为它真的值得,而不是让无序的 exec 做它的事情。特别是如果您有超线程,因此这种受延迟限制的过程可能发生在 CPU 的一个逻辑核心上,该核心也在做其他事情。