为了满足某些安全属性,我想确保一个重要数据在语句访问时已经在缓存中(因此不会有缓存未命中) . 例如,对于此代码
...
a += 2;
...
我想确保 a
在执行 a += 2
之前就在缓存中 .
我正在考虑使用x86的 PREFETCHh
指令来实现这个目的:
...
__prefetch(&a); /* pseudocode */
a += 2;
...
但是,我已经读过,在 a += 2
之前插入预取指令可能为时已晚,以确保执行 a += 2
时 a
在缓存中 . 这个说法是真的吗?如果是,我可以通过在预取之后插入 CPUID
指令来修复它,以确保执行了prefectch指令(因为英特尔手册说 PREFETCHh
是针对 CPUID
订购的)?
1 回答
是的,您需要预先获取大约内存延迟的提前期,以使其达到最佳状态 . 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
作为写预取,否则就是近无操作 . (这是一种非原子读 - 修改 - 重写) . 我不确定如果你做PREFETCHW
/CPUID
/add [mem], 2
,PREFETCHW
是否真的能确保缓存行准备就绪 . insn是wrt订购的 . CPUID,但手册并未说明预取效果是否已订购 .如果
a
是volatile
,则(void)a;
将获得gcc或clang以发出加载insn . 我假设大多数其他编译器(MSVC?)是相同的 . 您可以执行(void) *(volatile something*)&a
取消引用指向volatile
的指针并强制从a
的地址加载 .To guarantee that a memory access will hit in cache, you'd need to be running at realtime priority pinned to a core that doesn't receive interrupts . 根据操作系统的不同,定时器中断处理程序可能足够轻,以至于从缓存中驱逐数据的可能性足够低 .
如果您的进程在执行预取insn和执行实际访问之间进行了调度,则数据可能已从至少L1缓存中逐出 .
所以你不可能击败一个决心对你的代码进行定时攻击的攻击者,除非以实时优先级运行是现实的 . 攻击者可以运行许多内存密集型代码的线程......