是的,您需要以大约内存延迟的前置时间进行预取,以使其达到最佳状态。 Ulrich Drepper 的 What Every Programmer Should Know About Memory 谈了很多关于预取的内容。
对于单次访问来说,实现这一点非常重要。太快了,您的数据可能会在您关心的 insn 之前被驱逐。太晚了,它可能会减少一些访问时间。调整这将取决于编译器版本/选项,以及您正在运行的硬件。 (更高的每周期指令意味着您需要更早地预取。更高的内存延迟也意味着您需要更早地预取)。
由于您想对a 进行读-修改-写操作,因此您应该使用PREFETCHW(如果有)。其他预取指令仅用于读取预取,因此 RMW 的读取部分可能会命中,但我认为存储部分可能会因 MOSI 缓存一致性获得缓存行的写入所有权而延迟。
如果a 不是原子的,您也可以提前加载a 并在寄存器中使用副本。在这种情况下,返回全局的存储很容易丢失,但最终可能会停止执行。
你可能很难用编译器可靠地做一些事情,而不是自己编写 asm。任何其他想法也需要检查编译器输出以确保编译器完成了您希望的操作。
预取指令不一定要预取任何东西。它们是“提示”,当未完成加载的数量接近最大值(即几乎没有加载缓冲区)时,它们可能会被忽略。
另一种选择是加载它(不仅仅是预取),然后使用CPUID 进行序列化。 (丢弃结果的负载就像预取)。加载必须在序列化指令之前完成,序列化 insn 之后的指令在此之前无法开始解码。我认为预取可以在数据到达之前退出,这通常是一个优势,但在这种情况下,我们不关心一个操作会以牺牲整体性能为代价。
来自英特尔的 insn 参考手册(参见 x86 标签 wiki)条目中的 CPUID:
序列化指令执行
保证对先前指令的标志、寄存器和内存的任何修改都在之前完成
获取并执行下一条指令。
我认为这样的序列相当好(但仍然不能保证抢先式多任务系统中的任何东西):
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,我不确定PREFETCHW 是否真的会确保缓存行准备就绪。 insn 是按 wrt 排序的。 CPUID,但是手册上并没有说预取效果是有序的。
如果a 是volatile,那么(void)a; 将让gcc 或clang 发出一个load insn。我假设大多数其他编译器(MSVC?)都是相同的。您可以使用(void) *(volatile something*)&a 取消引用指向volatile 的指针并强制从a 的地址加载。
为了保证内存访问会在缓存中命中,您需要以固定到不接收中断的内核的实时优先级运行。根据操作系统的不同,定时器中断处理程序可能足够轻量级,以至于从缓存中逐出数据的机会足够低。
如果您的进程在执行预取 insn 和进行实际访问之间被取消调度,则数据可能至少已从 L1 缓存中被逐出。
因此,除非以实时优先级运行是现实的,否则您不太可能击败决心对您的代码进行定时攻击的攻击者。攻击者可以运行许多线程的内存密集型代码...