预取指令行为

Prefetch instruction behavior

为了满足一些安全性属性,我想确保一个重要的数据在语句访问它时已经在缓存中(所以不会有缓存未命中)。例如,对于此代码

...
a += 2;
...

我想确保 a 在执行 a += 2 之前就在缓存中。

我正在考虑使用x86的PREFETCHh指令来实现这个:

...
__prefetch(&a);     /* pseudocode */
a += 2;
...

但是,我了解到在 a += 2 之前插入预取指令可能为时已晚,无法确保 a 在执行 a += 2 时位于缓存中。这种说法是真的吗?如果是真的,我可以通过在预取后插入一条 CPUID 指令来修复它,以确保 prefectch 指令已被执行(因为英特尔手册说 PREFETCHh 是相对于 CPUID 排序的) ?

是的,您需要预取大约内存延迟的提前期才能使其达到最佳状态。 Ulrich Drepper 的 What Every Programmer Should Know About Memory 谈了很多关于预取的内容。

实现这一点对于单次访问来说非常重要。太快了,您的数据可能会在您关心的 insn 之前被驱逐。太晚了,它可能会减少一些访问时间。对此进行调整将取决于编译器 version/options 以及您 运行 正在使用的硬件。 (更高的每周期指令数意味着您需要更早地预取。更高的内存延迟也意味着您需要更早地预取)。

由于您想对 a 进行读取-修改-写入,您应该使用 PREFETCHW(如果可用)。其他预取指令仅为读取进行预取,因此 RMW 的读取部分可能命中,但我认为存储部分可能会延迟 MOSI 缓存一致性获得缓存行的写入所有权。

如果 a 不是原子的,您也可以提前加载 a 并在寄存器中使用副本。在这种情况下,返回全局的存储很容易丢失,但最终可能会停止执行。

您可能很难使用编译器可靠地完成某些工作,而不是自己编写 asm。任何其他想法还需要检查编译器输出以确保编译器执行了您希望的操作。

预取指令不一定预取任何内容。它们是 "hints",当未完成的负载数量接近最大值(即几乎用完负载缓冲区)时,它们可能会被忽略。


另一种选择是加载它(不仅仅是预取),然后使用 CPUID 进行序列化。 (丢弃结果的加载就像预取)。加载必须在序列化指令之前完成,序列化insn之后的指令直到那时才能开始解码。我认为预取可以在数据到达之前退出,这通常是一个优势,但在我们关心一个操作以牺牲整体性能为代价的情况下不是这样。

来自英特尔的 insn ref 手册(参见 标签 wiki)CPUID 条目:

Serializing instruction execution guarantees that any modifications to flags, registers, and memory for previous instructions are completed before the next instruction is fetched and executed.

我认为这样的序列相当不错(但在抢占式多任务系统中仍然不能保证任何事情):

add [mem], 0        # can't retire until the store completes, requiring that our core owns the cache line for writing
CPUID               # later insns can't start until the prev add retires
add [mem], 2        # a += 2   Can't miss in cache unless an interrupt or the other hyper-thread evicts the cache line before this insn can execute

这里我们使用 add [mem], 0 作为写预取,否则几乎是空操作。 (这是一个 非原子 读取-修改-重写)。我不确定如果你 PREFETCHW / CPUID / add [mem], 2 是否真的会确保缓存行准备就绪。该 ins 被订购 wrt。 CPUID,但是手册上没有说预取效果是有序的


如果 avolatile,那么 (void)a; 将让 gcc 或 clang 发出加载 insn。我假设大多数其他编译器(MSVC?)是相同的。您可能可以执行 (void) *(volatile something*)&a 取消引用指向 volatile 的指针并强制从 a 的地址加载。


为了保证内存访问将命中缓存,您需要运行将实时优先级固定到核心不接收中断。根据 OS,计时器中断处理程序可能足够轻量级,因此从缓存中逐出数据的可能性足够低。

如果您的进程在执行预取 insn 和进行实际访问之间取消调度,则数据可能至少已从 L1 缓存中逐出。

因此,您不太可能击败决心对您的代码进行定时攻击的攻击者,除非实时优先级 运行 是现实的。攻击者可以 运行 许多内存密集型代码线程...