内存方面还有一种优化方案,是预取。但是预取存在较多的局限性,需要把握好度和时机,虽然大多时候软件开发不需要关心底层硬件或者cache的预取机制,但是懂得它的原理对我们进行性能优化有很大的帮助。
1、预取
1.1 预取原理
时间局部性原理:系统即将用到的指令/数据可能就是当前使用的指令/数据
空间局部性原理:系统即将使用到的指令/数据可能就是相邻地址正在使用的
1.2 数据cache预取条件
cache预取需要满足以下条件:
(1)读写的数据是回写的,这是因为如果采用直写,会不停的把数据覆盖,预取是通过当前的数据去预计未来执行的数据,当前的数据不停的覆盖对未来执行的数据没有意义
(2)预取的请求必须在一个4K物理页的内部。这是因为对于程序员来说,虽然指令和数据的虚拟地址都是连续的,但是分配的物理页很有可能是不连续的。而预取是根据物理地址进行判断的,因此跨界预取的指令和数据很有可能是属于其他进程的,或者没有被分配的物理页
(3)处理器的流水线作业中没有fence或者lock这样的指令。这是说明流水线作业中没有其它进程,不存在竞争,如果存在竞争关系的话,当前的数据可能是另一个进程的,预取也就没有意义
(4)当前读取的预取指令没有很多不命中的情况,说明预取是生效的
(5)前端总线不是很忙,因为预取存在时间局部性,如果总线很忙,使得指令不能及时预取,违背时间局部性原理
(6)没有连续的存储(Store)指令。这个和第一条相似
1.3 硬件的预取条件
在Netburst架构的处理器中,硬件遵循以下原则来决定是否开启自动预取。
(1)必须是连续两次cache不命中,并且两次不命中的内存地址的位置偏差不能超过256或者512字节才会激活预取机制。这条有两个重点,连续两次不命中,并且地址偏差还不能太大。通过这两个条件说明处理的数据可能是一个连续的数据,即使当前连续的两个数据没有命中缓存,但根据局部性原理(空间局部性和时间局部性),接下来很可能要访问的数据依然位于这个连续的数据流中。预取机制在此时提前加载这些未来可能访问的数据到高速缓存,可以减少等待内存读取的时间延迟,从而提高处理器执行指令的速度。
(2)一个4K字节的页(Page)内,只定义一条流(Stream,可以是指令,也可以是数据)。因为处理器同时能够追踪的流是有限的。这个目的的作用是在处理单独的数据时,必须是单独的线程,这样预取才有意义。
(3)对4K字节的边界之外不进行预取。也就是说,预取只会在一个物理页(4K字节)内发生。这和一级数据Cache预取遵循相同的原则。
(4)预取的数据存放在二级或者三级Cache中
1.4 软件预取
2、cache一致性
2.1 cache line对齐
就是定义该数据结构或者数据缓冲区时就申明对齐
2.2 一致性协议解决一致性问题
2.2.1 基于目录协议
基于目录协议的系统中,需要缓存在Cache的内存块被统一存储在一个目录表中,目录表统一管理所有的数据,协调一致性问题。该目录表类似于一个仲裁者,当处理器需要把一个数据从内存中加载到自己独占的Cache中时,需要向目录表提出申请;当一个内存块被某个处理器改变之后,目录表负责改变其状态,更新其他处理器的Cache中的备份,或者使其他处理器的Cache的备份无效。
2.2.2 MESI协议
2.3 dpdk如何保证cache的一致性
DPDK的解决方案很简单,首先就是避免多个核访问同一个内存地址或者数据结构。这样,每个核尽量都避免与其他核共享数据,从而减少因为错误的数据共享(cache line false sharing)导致的Cache一致性的开销。说的简单一点,什么操作都是单独使用,比如线程的单独的收发包队列,线程利用亲和性绑核,使得资源在处理时不会存在竞争。