越往上,代表的是访问速度越快,当然存储容量小,价格也非常的高。越往下,意味着访问速度越慢,存储容量大,价格相对便宜。通常我们CPU的寄存器是L1的高速缓存,L1是L2的高速缓存,以此类推。
上图我们把k+1理解为主存,被划分为16个块来存储数据,块的大小是固定的。我们把K层理解成L3高速缓存,任何时刻L3就是主存的一个子集。上图我们能看出,L3只能保存4个块的数据,块的大小保持和主存的大小一样的。上图中我们看到,L3中保存的是主存中的4,9,14,3的数据。那么什么又是命中率和不命中率呢?
缓存命中:当程序需要第k+1层数据块14的时候,程序会在当前存储的k层,寻找块14的数据,刚好14在k层的话,就是一个缓存命中,这比从k+1层读取的速度要快很多。
缓存不命中:当程序需要访问到块12的时候,在k层没有该数据块,就是一个缓存不命中(cache miss),这时候就会从k+1层中读取块12将其替换到k层的一个数据块(覆盖或驱逐一个已有的数据块)。程序还是从k层访问块12。
放置策略:如果我们从k+1层中获得的数据随机的放置在k层,这样的随机放置就会导致访问的效率降低,我们的放置策略是块i必须放置在(imod4)中,也就是0,4,8,12会映射到同一个k层的块0中。这就会导致一个冲突不命中,也就是说如果程序交替请求k+1层的0,4块,由于会一直映射到k层的0块中,这时候虽然k层有空余的缓存,但还是每次不命中。
总结:利用时间的局部性,同一数据对象可能会被多次使用,一旦一个数据对象在第一次不命中的时候被拷贝到缓存中,我们就会期望在接下来的访问中有一系列的命中率。利用空间的局部性,由于一个数据块并不仅仅只有一个数据,而是一系列数据块的集合,我们访问到块子集a的时候,可能会继续访问块的子集b。
1) cache基本概念
2) Cache的存取单位(Cache Line)
3) Cache的工作模式
4) 内存一致性
write back会涉及内存一致性,涉及到一系列的问题:
多处理要系统更新cache时,一个处理器修改了cache的内容,第二个处理器将不能访问这个cache,直到这个cache的内容被写内存。在现代处理器中硬件已经做了精心的设计,确保这种事情不会发生,硬件负责保持cache在各个CPU之间一致.
外围硬件设备可以通过DMA(Direct Memory Access)访问内存,而不让处理器知道,也不会利用cache,这样在内存和cache之间就会出现不同步的情况。管理DMA的操作是操作系统的工作,比如设备驱动程序,它将保证内存与cache的一致性.
当在cache中的数据比内存中的数据老时,称为stale。如果软件初始化DMA,使设备和RAM之间传递数据,那么软件必须告诉 CPU,cache中的条目必须失效.
当在cache中的数据比内存中的数据新时,称为dirty。在设备驱动程序允许一个设备经DMA从内存读数据时,它必须确保所有的dirty 条目写进内存,也叫做flushing或sync cache
Cache与TLB本质上都是利用数据访问的局部性原理,就是把最常用的数据放在最快可以访问的地方。有所不同的是,Cache存放的是内存中的数据或者代码,而TLB存放的是页表项。
提到页表项,有必要简短介绍一下处理器的发展历史。最初的程序员直接对物理地址编程,自己去管理内存,这样不仅对程序员要求高,编程效率低,而且一旦程序出现问题也不方便进行调试。特别还出现了恶意程序,这对计算机系统危害实在太大,因而后来不同的体系架构推出了虚拟地址和分页的概念。
具体而言:
Cache与TLB谁先访问?
通常来说,CPU发出的数据访问请求都是虚拟地址的,那CPU发出的数据请求是先到Cache中查找有无该虚拟地址对应的数据还是先利用MMU进行虚拟地址到物理地址转换然后再查找Cache呢?答案是上述2种情况都有可能,这取决于TLB与Cache的先后顺序。
物理Cache与逻辑Cache各有优缺点。
对于逻辑Cache:
(1)CPU可以更快得知数据是否被cache,因为不需要等待MMU进行虚拟地址到物理地址的转换。
(2)一方面,由于不同进程之间会存在相同的虚拟地址。另一方面,不同的虚拟地址可能会对应相同的物理地址。因此,逻辑Cache在进行进程上下文切换时需要flush cache或者通过在每一个cache line中添加额外的位来区分各个进程或者物理地址对应的Cache。
对于物理Cache:
(1)CPU获取被Cache缓存的数据速度更慢,因为需要先利用MMU进行虚拟地址到物理地址的转换,然后才查找cache。
(2)每一个cache line的位数可以更少,因为物理地址是唯一的,不像虚拟地址那样会出现重复,而且在进程上下文切换时也不必flush cache。
使用大页
当cpu对数据进行读请求时, cpu根据虚拟地址(前20位)到TLB中查找。TLB中保存着虚拟地址(前20位)和页框号(页框号可以理解为页表项)的对映关系,如果匹配到虚拟地址就可以迅速找到页框号,,通过页框号与虚拟地址后12位的偏移组合得到最终的物理地址。
如果没在TLB中匹配到逻辑地址,就出现TLB不命中,需要到页表中查询页表项,进行常规的查找过程。如果TLB足够大,那么这个转换过程就会变得很快速。但是事实是,TLB是非常小的,一般都是几十项到几百项不等,并且为了提高命中率,很多处理器还采用全相连方式。另外,为了减少内存访问的次数,很多都采用回写的策略。
举个例子。如果想支持32位的操作系统下的4GB进程虚拟地址空间,假设页表大小为4K,则共有2^20次方页面。如果采用速度最快的1级页表,对应则需要2 ^20次方个页表项。一个页表项假如4字节,那么一个进程就需要(1048576x4)4M的内存来存页表项。如果采用如图2级页表,则只需要页目录1024个,页表项1024个,总共2048个页表管理条目,8k(2048x4)就可以支持起4GB的地址空间转换。
但是,TLB大小毕竟是很有限的,随着程序的变大或者程序使用内存的增加,那么势必会增加TLB的使用项,最后导致TLB出现不命中的情况。那么,在这种情况下,大页的优势就显现出来了。对于消耗内存以GB(2^30)为单位的大型程序,可以采用1GB为单位作为分页的基本单位,4G进程虚拟地址只要4个页面就够了,可以有效减少TLB不命中的情况。
服务器是如何处理从网络上来的数据?
可以看出,对于一个数据报文,CPU和网卡需要多次访问内存。而内存相对CPU的使用寄存器来讲是一个非常慢速的部件。CPU需要等待数百个周期才能拿到数据,在这过程中,CPU什么也做不了。
DDIO(Direct I/O)技术使外部网卡和CPU通过LLC Cache直接交换数据,绕过了内存这个相对慢速的部件。这样,就增加了CPU处理网络报文的速度(减少了CPU和网卡等待内存的时间),减小了网络报文在服务器端的处理延迟。这样做也带来了一个问题,因为网络报文直接存储在LLC Cache中,这大大增加了对其容量的需求,因而在英特尔的E5处理器系列产品中,把LLC Cache的容量提高到了 20MB。
利用perf stat 分析 cache miss:
https://blog.csdn.net/qq_15437629/article/details/117824718?spm=1001.2014.3001.5502
https://blog.csdn.net/qq_15437629/article/details/77822040
前面介绍了一些cache的相关背景知识,这里总结一下DPDK为了提升性能,在cache方面有哪些需要注意的点。
①适时的使用cache预取技术。在处理网卡队列的时候,多数时候都是在处理连续的内存,此时应该主动调用预取函数,可以提升效率。
和缓存预取有关的指令:
指令 Description
PREFETCHT0 预取数据到所有级别的缓存,包括L0。
PREFETCHT1 预取数据到除L0外所有级别的缓存。
PREFETCHT2 预取数据到除L0和L1外所有级别的缓存。
PREFETCHNTA 预取数据到非临时缓冲结构中,可以最小化对缓存的污染。和PREFETCHT0 功能类似,但是数据在使用完
在l3fwd中使用预取的代码段
/ *
*从RX队列读取数据包
* /
for(i = 0; i <qconf-> n_rx_queue; ++ i){
portid = qconf-> rx_queue_list [i] .port_id;
queueid = qconf-> rx_queue_list [i] .queue_id;
nb_rx = rte_eth_rx_burst(portid,queueid,pkts_burst,MAX_PKT_BURST);
/ *预取第一个数据包* /
for(j = 0; j <PREFETCH_OFFSET && j <nb_rx; j ++){
rte_prefetch0(rte_pktmbuf_mtod(
pkts_burst [j],void *));
}
/ *预取并转发已经预取的数据包* /
for(j = 0; j <(nb_rx-PREFETCH_OFFSET); j ++){
rte_prefetch0(rte_pktmbuf_mtod(pkts_burst [
j + PREFETCH_OFFSET],void *));
l3fwd_simple_forward(pkts_burst [j],portid,qconf-> lookup_struct);
}
/ *转发剩余的预取包* /
for(; j <nb_rx; j ++){
l3fwd_simple_forward(pkts_burst [j],portid,qconf-> lookup_struct);
}
②为了解决cache一致性问题,数据结构尽量声明为cache line对齐。并且多核访问的数据,可以设计成每CPU变量。
当定义的数据结构或者分配了数据缓冲区之后,内存中就有了一个地址和其相对应,然后程序进行读写。在读的过程中,首先是内存加载到Cache,随后送到处理器内部的寄存器;在写操作的时候则是从寄存器送到Cache,最后由总线回写到内存。这样会出现两个问题:
1)数据结构/数据缓冲区对应的Cache Line是否对齐?如果不是的话,即使数据区域小于Cache Line的话也会占用两个Cache Line;另外假如上一个CacheLine属于另一个数据结构且被另一个处理器核处理,数据如何同步呢?
2)假设数据结构/缓冲区的起始地址是CacheLine对齐的,但是有多个核同时对该内存进行读写,如何解决冲突?
DPDK解决方案:
1)提供 rte_cache_aligned 强制对齐缓存行;
2)避免多个核访问同一个内存地址或者数据结构。每个核尽量避免与其他核共享数据,从而减少因为错误的数据共享导致的Cache一致性开销。
如DPDK官方的l3fwd为例:
struct lcore_conf {//保存lcore的配置信息
uint16_t n_rx_queue; //接收队列的总数量
struct lcore_rx_queue rx_queue_list[MAX_RX_QUEUE_PER_LCORE];//物理端口和网卡队列编号组成的数组
uint16_t tx_queue_id[RTE_MAX_ETHPORTS]; //发送队列的编号组成的数组
struct mbuf_table tx_mbufs[RTE_MAX_ETHPORTS];//mbuf表
lookup_struct_t * ipv4_lookup_struct; //实际上就是struct rte_lpm *
#if (APP_LOOKUP_METHOD == APP_LOOKUP_LPM)
lookup6_struct_t * ipv6_lookup_struct;
#else
lookup_struct_t * ipv6_lookup_struct;
#endif
} __rte_cache_aligned;
以上的数据结构 “struct lcore_conf” 总是CacheLine对齐,而定义数组“lcore[RTE_MAX_LCORE]”中RTE_MAX_LCORE为系统中最大核的数量。DPDK对每一个核编号,这样核n就只需要访问lcore[n],避免了多个核访问同一结构体。
多核的情况下,有可能多个核访问同一个网卡的接收/发送队列,这样也会引起Cache一致性的问题。DPDK就会为每个核都准备一个单独的接收/发送队列
③系统环境中开启大页
④使用支持DDIO技术的服务器。
⑤在NUMA系统中,为每个核分配内存,并且连接在某处理器上的PCI设备,让本地处理器来处理。
另外关注一个报文,从网卡收到,送到CPU,再到CPU送到网卡转发出去的全部过程:
①CPU把接收描述符写入内存。
②网卡收到报文后写入描述符。引入DDIO后直接写入cache,如果本身这块内存不在cache,也会在这时把这块内存放入cache。
③CPU从内存读更新后的描述符。确认收到报文再从内存读控制结构体指针,再根据指针从内存中读控制结构体。使用预取指令,直接把后续报文的内存也都拿到cache中。
④更新接收队列寄存器。
⑤CPU读到报文首地址,根据首地址读报文头,决定转发端口。引入DDIO时直接从cache读到。
⑥填入内存中的发送描述符,更新发送队列寄存器。引入DDIO后,此时描述符已经预取到cache了。
⑦CPU读内存看发送描述符,检查是否有报文被传出去了,有的话再读控制结构体,释放数据缓冲区。引入DDIO后可以直接读cache