软件预取手动指令是合理的情况

时间:2016-11-11 14:13:35

标签: c++ gcc cpu-cache prefetch

我在x86和x86-64上已经了解到这一点,英特尔gcc提供了特殊的预取指令:

#include <xmmintrin.h>
enum _mm_hint
{
_MM_HINT_T0 = 3,
_MM_HINT_T1 = 2,
_MM_HINT_T2 = 1,
_MM_HINT_NTA = 0
};
void _mm_prefetch(void *p, enum _mm_hint h);

程序可以在任何内容上使用_mm_prefetch内在函数 程序中的指针。以及_mm_prefetch使用的不同提示 内在的是实现定义。一般来说,每个提示都有其自身的含义。

  

_MM_HINT_T0   将数据提取到缓存的所有级别以用于包含缓存   以及独占缓存的最低级别缓存

     

_MM_HINT_T1 提示将数据拉入L2和   不进入L1d。如果存在L3缓存 _MM_HINT_T2   提示可以为它做类似的事情

     

_MM_HINT_NTA ,允许告诉处理器专门处理预取的缓存行

当使用此指令时,有人可以描述示例吗?

如何正确选择提示?

1 个答案:

答案 0 :(得分:5)

预取的想法是基于这些事实:

  • 第一次访问内存非常昂贵 第一次访问内存地址 1 时必须从内存中获取,然后将其存储在缓存层次结构 2 中。
  • 访问内存本质上是异步的 CPU不需要来自内核的任何资源来执行加载/存储 3 的最长部分,因此可以与其他任务 4

由于上述原因,在实际需要之前尝试加载是有意义的,这样当代码实际需要数据时,它就不必等待。
在寻找要做的事情时,CPU可以走得很远,但不是任意深度,这是非常值得的。所以有时它需要程序员的帮助才能达到最佳状态。

就其本质而言,缓存层次结构是微架构的一个方面,而不是架构(读取ISA)。英特尔或AMD无法对这些说明的作用给予强有力的保证 此外,正确使用它们并不容易,因为程序员必须清楚每条指令可以采用多少个周期。 最后,最新的CPU越来越擅长隐藏内存延迟并降低内存延迟 因此,通常预取是熟练的汇编程序员的工作。

这就是说唯一可能的情况是一段代码的时间必须在每次调用时保持一致 例如,如果您知道中断处理程序总是更新状态并且它必须尽可能快地执行,那么在设置使用此类中断的硬件时,值得预取状态变量。

关于不同级别的预取,我的理解是不同级别(L1 - L4)对应于共享污染的不同数量。

例如,如果执行指令的线程/核心与读取变量相同,则prefetch0是好的。
但是,这将在所有缓存中占用一条线,最终驱逐其他可能有用的线。 例如,当您知道您确实需要数据时,可以使用它。

prefetch1可以快速为所有核心或核心组提供数据(取决于如何共享L2),而不会污染L1。
如果您知道可能需要数据,或者在完成其他任务(优先使用缓存)之后您需要数据,则可以使用此功能。
这不如在L1中使用数据那么快,但比在内存中使用数据要好得多。

prefetch2可用于取出大部分内存访问延迟,因为它会在L3缓存中移动数据。
它不会污染L1或L2并且它在内核之间共享,因此它很适合稀有(但可能)代码路径使用的数据或为其他内核准备数据。

prefetchnta最容易理解,它是非暂时移动。它避免在每个缓存行中为只访问一次的数据创建一个条目。

prefetchw/prefetchwnt1与其他人一样,但会使该行独占,并使其他核心行无效。 基本上,它使写入速度更快,因为它处于MESI协议的最佳状态(缓存一致性)。

最后,可以逐步完成预取,首先移入L3,然后移入L1(仅适用于需要它的线程)。

简而言之,每条指令都可以让您决定污染,分享和访问速度之间的妥协 由于这些都需要非常仔细地跟踪缓存的使用(您需要知道它不值得在L1中创建和输入,但它在L2中),因此仅限于非常特定的环境。
在现代操作系统中,无法跟踪缓存,您可以执行预取只是为了找到您的量程已过期而您的程序被另一个驱逐刚刚加载的线路的程序取代。

至于一个具体的例子,我有点想法 在过去,我必须尽可能一致地衡量一些外部事件的时间 我使用和中断来定期监视事件,在这种情况下,我预取了中断处理程序所需的变量,从而消除了第一次访问的延迟。

使用预取的另一种非正统方法是将数据移动到缓存中 如果要测试缓存系统或从依赖缓存的内存中取消映射设备以使数据保持更长时间,这非常有用。
在这种情况下移动到L3就足够了,并非所有CPU都有L3,所以我们可能需要转移到L2。

我理解这些例子并不是很好。

1 实际上,粒度是&#34;缓存行&#34;不是&#34;地址&#34;。
2 我认为你很熟悉。简而言之:它目前从L1到L3 / L4。 L3 / L4在核心之间共享。 L1始终是每个核心私有并由核心线程共享,L2通常类似于L1,但某些模型可能在两对核心之间共享L2。 3 最长的部分是来自RAM的数据传输。计算地址和初始化事务会占用资源(例如,存储缓冲区槽和TLB条目) 4 但是@Leeor和proved by the Linux kernel developer指出,用于访问内存的任何资源都可能成为一个关键问题。