DPDK学习——cache相关知识③
4. cache预取
背景:cache操作对大多数程序员透明,但当对程序执行效率有很高要求的时候,程序员可以一定程度上控制cache。
4.1 Cache局部性
①时间局部性:程序即将用到的指令或数据,可能就是目前正在使用的指令或数据,可以在当前的指令数据使用完后暂存在cache中。典型的例子就是for循环。
②空间局部性:程序即将使用的指令或数据可能与目前正在使用的指令或数据在空间上相邻或相近。所以可以在处理当前指令数据时,把内存中相邻区域的指令或数据也读到cache里。典型的例子就是处理数组。
4.2 预取
NetBurst架构硬件预取:有两个硬件预取单元,分别对应数据和指令,**后可以自动预取下一个cache line的数据。预取只发生在同一个页内,不会预取到其他页。
实例:对二维数组arr[1024][1024]赋值
for(int i = 0; i < 1024; i++) for(int j = 0; j < 1024; j++) arr[i][j] = 1; }
|
for(int i = 0; i < 1024; i++) for(int j = 0; j < 1024; j++) arr[j][i] = 1; }
|
左边的在内存中顺序访问,硬件可以识别规律,完成预取,提高效率。而右边的代码对内存的访问时跳跃的,硬件无法识别,造成效率低下。
软件预取指令可以在需要的地方显式的加载数据到cache。但如果滥用,会导致cache负载过高,无用数据比例增加,反而造成效率低下。
预取指令:PREFETCH0、PREFETCH1、PREFETCH2,第一个指令会把数据放在每一级cache,即如果有三级cache,L1、L2、L3都会有一份备份;第二个指令不会在L1中备份,第三个指令不会在L1、L2中备份。PREFETCHNTA指令会存在每一级cache,但是一旦使用后就会从cache里移出。
我们也有封装好的C语音函数void__mm_prefetch(char* p, int i)。p是需要预取的内存地址,i对应预取指令。
4.3 报文处理过程
转发基本过程(RX——分析——TX):
①写接收描述符到内存,描述符包含一个报文内存的起始地址,当网卡收到报文后,会把报文内容写入这个地址。
②处理器读内存中的接收描述符。
③接收描述符中有内存,读控制结构体指针,根据指针读控制结构体(这里读了两次内存)。
④更新接收队列寄存器。
⑤从内存中读报文头部。这里分析报文,决定转发端口。
⑥通过控制结构体把报文写入发送队列发送描述符,更新发送队列寄存器。
⑦从内存读发送描述符,检查是否有报文发送。
⑧如果发了,从内存中读控制结构体,释放数据。
一个完整的转发流程需要读6次内存,每读一次内存需要几百个时钟周期,而我们需要每80个时钟周期就要处理一个报文,所以必须保证所有报文都能从cache里读到,不容miss。
DPDK:书中给出了ixgbe_rxtx.c中ixgbe_recv_pkts()函数的代码片段,每处理一个预取下一个mbuf和报文,每处理4个ring预取一次接收描述符和控制结构体指针。
5. Cache一致性
5.1 背景
程序读数据时,数据先从内存加载到cache,再送到处理器内存的寄存器;写时,寄存器送到cache,再写回内存。
因为引发两个问题:①如果这个数据结构不是cache line对齐,可能出现一个cache line前半部分属于一个数据结构,后半部分属于另一个数据结构,当有两个核分别处理这两个数据结构时,去往内存同步数据时,如何同步?②如果cacheline对齐了,但是多个核同时读写,同时回写,如何解决冲突?
5.2 cache line对齐
如果一个cache line里有两个数据结构,就可能造成多个核同时操作一个cache line,那最好的办法就是数据结构在定义时就声明成cache line对齐的,也就是64字节对齐。用__rte_cache_aligned表示。它实质上就是__attribute__((__aligned__(64)))。
5.3 cache一致性
问题产生原因的根源是存在多个处理器独占的cache。这导致一个内存同时在多个cache中有备份。如果有多个处理器,但是共享cache,那么每个指令周期内只有一个处理器核心可以通过cache读写内存,就不会有问题。
所有处理器共享cache的话可以一劳永逸的解决这个问题,但是这样①共享cache必须容量大,访问比每处理器独占时慢②每时钟周期内只有一个处理器可以访问cache。故而不可以共享cache。
一致性协议可以解决一致性问题。一种是基于目录的协议,维护一个目录表,一个处理器改变了一个内存块之后,目录表会改变这个内存块的状态,让所有其他处理器都更新状态,或者让其他处理器手上的内存块备份无效。
另一种叫总线窥探协议,一个处理器负责监听总线,每次更改内存块都后广播出去;每接收到总线通知后,就更新本地备份的状态。
MESI协议是常用的总线窥探协议。是cache line四种状态的首字母,它们是Modified,Exclusive,Shared,Invalid。
M修改态:如果某个cache line在多个cache中有备份,那只有一个备份能处于M状态,且置dirty位。修改态的cacheline处于一个准备把内容写回到内存的状态。在写回之前这部分cache line对应的内存块不能被任何处理器读取。
E独占态:多个备份中只有一个可以处于这个状态,但dirty未置。产生读请求时变为共享态,写请求时变成修改态。
S共享态:说明这个cache line在多个cache中有备份
I无效态:说明该cache line已经失效,他可能已经移出cache,也可能已经过时,总之这个状态的cache line可以当做从来没被加载到cache过。
状态转换:
*总线读意味着从总线上监测到有其他处理器在请求读该cache line;总线写同理。处理器读写指本地处理器对本cache的读写操作。
5.4 DPDK如何保证cache一致性:
每个核尽量不与其他核共享数据。可以参考lcore[RTE_MAX_LCORE]的定义,以及网卡队列的分配。DPDK中每个核都有对应一个网卡的单独的发送接收队列。
小结
本文先介绍了cache预取机制,让程序员可以控制cache,以提高效率。然后承接上文,介绍了如何保证cache的一致性,主要介绍了cache line对齐和MESI协议。