预取的想法基于以下事实:
- Accessing memory is very expensive the first time.
The first time a memory address1 is accessed is must be fetched from memory, it is then stored in the cache hierarchy2.
- Accessing memory is inherently asynchronous.
The CPU doesn't need any resource from the core to perform the lengthiest part of a load/store3 and thus it can be easily done in parallel with other tasks4.
由于上述原因,在实际需要数据之前尝试加载是有意义的,这样当代码实际需要数据时,就不必等待。
毫无意义的是,CPU 在寻找要做的事情时可以走得很远,但不能任意深入;所以有时需要程序员的帮助才能发挥最佳性能。
就其本质而言,缓存层次结构是微体系结构的一个方面,而不是体系结构(请参阅 ISA)。 Intel 或 AMD 无法对这些指令的作用提供强有力的保证。
此外,正确使用它们并不容易,因为程序员必须清楚每条指令可以占用多少个周期。
最后,最新的 CPU 越来越擅长隐藏内存延迟并降低延迟。
因此,一般来说,预取是熟练的汇编程序员的工作。
也就是说,唯一可能的情况是每次调用时一段代码的时间必须保持一致。
例如,如果您知道中断处理程序总是更新状态并且必须尽可能快地执行,则在设置使用此类中断的硬件时值得预取状态变量。
关于不同级别的预取,我的理解是不同级别(L1 - L4)对应不同数量的预取sharing and 污染的.
例如prefetch0
如果执行指令的线程/核心与读取变量的线程/核心相同,则很好。
然而,这将占用所有缓存中的一行,最终驱逐其他可能有用的行。
例如,当您知道您肯定需要简短的数据时,您可以使用此功能。
prefetch1
有利于使数据快速可供所有核心或核心组使用(取决于 L2 的共享方式),而不污染 L1。
如果您知道自己可以使用此功能may需要数据,或者在完成另一项任务(优先使用缓存)后需要它。
这不如将数据存储在 L1 中那么快,但比将数据存储在内存中要好得多。
prefetch2
由于它在 L3 缓存中移动数据,因此可用于消除大部分内存访问延迟。
它不会污染 L1 或 L2,并且在核心之间共享,因此它非常适合罕见(但可能)代码路径使用的数据或为其他核心准备数据。
prefetchnta
是最容易理解的,它是一个非时间性的移动。它避免了在每个缓存行中为仅访问一次的数据创建一个条目。
prefetchw/prefetchwnt1
与其他核心行类似,但使该行独占并使别名该行的其他核心行无效。
基本上,它使写入速度更快,因为它处于 MESI 协议的最佳状态(用于缓存一致性)。
最后,可以增量地完成预取,首先移动到 L3,然后移动到 L1(仅针对需要它的线程)。
简而言之,每条指令都让您决定污染、共享和访问速度之间的折衷。
由于这些都需要非常仔细地跟踪缓存的使用情况(您需要知道它不值得在 L1 中创建和输入,但它在 L2 中),因此使用仅限于非常特定的环境。
在现代操作系统中,不可能跟踪缓存,您可以进行预取,只是为了发现您的量程已过期,并且您的程序被另一个驱逐刚刚加载的行的程序所取代。
至于具体的例子,我有点没有想法。
过去,我必须尽可能一致地测量某些外部事件的时间。
我使用 和 中断来定期监视事件,在这种情况下,我预取了中断处理程序所需的变量,从而消除了第一次访问的延迟。
预取的另一种非正统用途是将数据移动到缓存中。
如果您想测试缓存系统或从内存中取消映射依赖于缓存的设备以保留数据更长的时间,这非常有用。
在这种情况下,转移到 L3 就足够了,并不是所有的 CPU 都有 L3,所以我们可能需要转移到 L2。
但我知道这些例子并不是很好。
1 Actually the granularity is "cache lines" not "addresses".
2 Which I assume you are familiar with. Shortly put: It, as present, goes from L1 to L3/L4. L3/L4 is shared among cores. L1 is always private per core and shared by the core's threads, L2 usually is like L1 but some model may have L2 shared across pairs of cores.
3 The lengthiest part is the data transfer from the RAM. Computing the address and initializing the transaction takes up resources (store buffer slots and TLB entries for example).
4 However any resource used to access the memory can become a critical issue as pointed out by @Leeor and proved by the Linux kernel developer https://lwn.net/Articles/444336/.