在加载数字流时避免缓存污染

Posted

技术标签:

【中文标题】在加载数字流时避免缓存污染【英文标题】:Avoiding cache pollution while loading a stream of numbers 【发布时间】:2016-10-20 02:05:12 【问题描述】:

在 x86 处理器上,有没有一种方法可以将数据从常规写回内存加载到寄存器中而无需通过缓存层次结构?

我的用例是我有一个大的查找结构(哈希图或 B 树)。我正在处理大量数字(比我的 L3 大得多,但适合内存)。我想做的很简单:

 int result = 0;
 for (num : stream_numbers) 
     int lookup_result = lookup_using_b_tree(num);
     result += do_some_math_that_touches_registers_only(lookup_result);
 
 return result;

由于我只访问每个数字一次并且所有数字的总和大于 L3 大小,我想他们最终会驱逐一些包含我的 B-tree 部分的缓存行。相反,我希望这个流命中缓存中没有任何数字,因为它们根本没有时间局部性(只读取一次)。这样我可以最大限度地提高我的 B-tree 保留在缓存中的机会并且查找速度更快。

我查看了 SSE 4.1 中可用的 (v)movntdqa 指令,用于时间负载。这似乎不太合适,因为它似乎只适用于不可缓存的写入组合内存。这个来自英特尔的老article 声称:

未来几代英特尔处理器可能会针对流式加载进行优化和增强,例如提高流式加载缓冲区的利用率并支持其他内存类型,从而为软件开发人员创造更多机会来提高其性能和能源效率应用程序。

但是我今天不知道有任何这样的处理器。我已经读过elsewhere,处理器可以选择忽略这个回写内存的提示,而是使用movdqa。那么有什么方法可以在不通过 x86 处理器上的缓存层次结构的情况下从常规写回内存中实现负载,即使它只能在 Haswell 和更高版本的模型上实现?我也很感激有关将来是否有可能的任何信息?

【问题讨论】:

最近有人问了一个类似的问题,你可能也对this one感兴趣。 AFAIK 没有可靠/有保证的方法来做到这一点。 prefetchnta 可能会有所帮助,但同样不清楚它是否可以做任何有用的事情,因为它不会覆盖 WB 内存类型的强排序缓存一致性语义。我认为你能期望的最好的结果是 prefetchnta 或 movntdqa 加载到缓存中并设置该行的 LRU 数据以表明它是一个很好的驱逐目标。因此,如果硬件确实以这种方式工作,那么一旦该流中的数据在每个集合中都有一个条目,希望该流中的数据只会从同一流中逐出之前的行。 如果您的数字的最终来源是文件或网络或程序外部的其他东西,您可能只想读取和处理更小块的数字。 @Rajiv: 不 :(。我的大部分知识都是理论知识,来自阅读文档/手册,而不是来自调整真实内容的实践经验。而且我不记得看到任何关于加速流媒体的内容从 WB 内存加载,只是存储。但就像我说的,我对微架构可以在 WB 内存上做什么的最佳猜测是设置缓存 LRU 数据,以便可以轻松地再次驱逐行。也许可以以某种方式测试这个猜测。跨度> 顺便说一下,这可能是一个有趣的阅读 - blog.stuffedcow.net/2013/01/ivb-cache-replacement。有可能您甚至不必为流式加载而烦恼,尽管当然只有基准测试才能证明这一点。 【参考方案1】:

是的,您可以使用MOVNTI 将值直接存储到内存中,而不会触及缓存。

MOVNTI 的延迟约为 400 个周期(在 Skylake 上)。 但是,如果您只是存储值,则您几乎不关心延迟,而更关心互惠吞吐量,即每个 MOVNTI 1 个周期。

请注意,您需要在完成商店后执行 SFENCE 或 MFENCE。

根据我对MOVNTI 的实验(在 ZeroMem 例程的上下文中),如果您编写的文件超过 512 KB,那么值得付出努力。 确切的值将主要取决于缓存大小等。

非时间性仅适用于写入,不适用于读取! 事实上,我不知道在读取数据时以非时间方式工作的任何 NT-mov 变体。

但是,如果您正在执行读取-修改-写入循环,则使用非时间移动几乎没有意义。 您还需要考虑节点结构的位置。 它可能看起来像这样:

left, right: pointer_to_node  (8 bytes aligned on 32 byte boundary).
data: integer;                (4 bytes) 
....

如果是这样,您读取 left/right 节点指针会将 data 吸进 32 字节 (*) 缓存行中。 只是对数据进行 NT-mov 在这里没有帮助,它在读取其他节点数据时已经被吸入,因此已经在缓存中。

编译器在缓存友好边界上对齐数据结构这一事实确保了最大数量的节点数据在每次节点指针访问时都被缓存到缓存中。

(*) 高速缓存行大小取决于处理器。

【讨论】:

movnti 不能作为负载工作。唯一的 NT 加载指令是MOVNTDQA。它不会覆盖排序语义,所以我认为它不能完全跳过缓存。 ***.com/a/37891933/224132 @PeterCordes,我经常按保存,所以您可以放心地忽略早期草稿:-)。据我了解,MOVNTDQA(阅读时)更多的是未来的证明,而不是实际的performs as advertized 功能。感谢您的提醒。 @PeterCordes 我认为在读取普通内存时不可能完全跳过缓存。但从理论上讲,有两件事可能有用。 prefetchntaclflushopt。如果prefetchnta 做它应该做的,它不会污染L2 和L3 缓存。 clflushopt 是 Skylake 的新成员,是旧 clflush 的快速(宽松有序)版本。所以你prefetchnta 进入L1。加载整个缓存行,然后将 clflushopt 加载出来。理论上不应该触及 L2/L3 并且将对 L1 的影响降到最低。 @Mysticial, movnti 在写入时会完全跳过缓存。但是在阅读时我同意。不确定 AMD 的受害者(即非 WC)L3/L2 缓存做了什么。 @Johan 流式写入是一个已解决的问题,因为movnti 和家人会按照您的期望行事。只是读起来很烦人。从根本上说,这是一个难题,因为读取会阻塞指令。

以上是关于在加载数字流时避免缓存污染的主要内容,如果未能解决你的问题,请参考以下文章

预加载、缓存 gif 图像以避免在 React Native 中闪烁

FragmentTabHost切换Fragment时避免UI重新加载

C++ 仅头文件库避免“使用命名空间”污染

[Effective JavaScript 笔记]第45条:使用hasOwnProperty方法以避免原型污染

Spring Security - 无法避免缓存控制

前端性能优化方法