cache和内存

Posted 造夢先森

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cache和内存相关的知识,希望对你有一定的参考价值。

存储器层次结构


越往上,代表的是访问速度越快,当然存储容量小,价格也非常的高。越往下,意味着访问速度越慢,存储容量大,价格相对便宜。通常我们CPU的寄存器是L1的高速缓存,L1是L2的高速缓存,以此类推。

高速缓存存储器

  • L1、L2、L3 cache属于高速缓存,集成在高速缓存存储器,是CPU内部的一个部件,学名静态随机存取存储器(Static Random Access Memory)。根据cache中存储的是指令还是数据还可以分为 I-cache 和 D-cache。i7的架构中,L1分为数据和指令高速缓存,共享L2高速缓存,同时每个核共享L3高速缓存。
  • 主存,也就是我们所谓的内存条,学名动态随机存取存储器(Dynamic Random Access Memory),是与CPU直接进行数据交换的内部存储器。其实内存条可以说是硬盘和CPU之间的一个中转站。
  • IO总线外面接着的就是磁盘控制器。对磁盘的数据访问,并不是直接从磁盘到CPU,而是通过主存作为桥梁,达到快速访问。

Intel Core I7高速缓存层次结构:

基本缓存原理

上图我们把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。

cache预取与cache一致性问题

1) cache基本概念

  • cache是为了解决处理器与慢速DRAM(慢速DRAM即内存)设备之间巨大的速度差异而出现的.
  • cache属于硬件系统,linux不能管理cache,但会提供flush整个cache的接口.
  • cache分为一级cache,二级cache,三级cache等等;一级cache与cpu处于同一个指令周期.

2) Cache的存取单位(Cache Line)

  • CPU从来不从DRAM直接读/写字节,从CPU到DRAM的每次读或写的第一步都要经过L1 cache,每次以整数行读或写到DRAM中.
  • Cache Line是cache与DRAM同步的最小单位。典型的虚拟内存页面大小为4KB,而典型的Cache line通常的大小为32或64字节.
  • CPU 读/写内存都要通过Cache,如果数据不在Cache中,需要把数据以Cache Line为单位去填充到Cache,即使是读/写一个字节.

3) Cache的工作模式

  • 数据回写(write-back):这是最高性能的模式,也是最典型的,在回写模式下,cache内容更改不需要每次都写回内存,直到一个新的 cache要刷新或软件要求刷新时,才写回内存.
  • 写通过(write-through):这种模式比回写模式效率低,因为它每次强制将内容写回内存,以额外地保存cache的结果,在这种模式写耗时,而读和回写模一样快,这都为了内存与cache相一致而付出的代价.
  • 预取 (prefectching):一些cache允许处理器对cache line进行预取,以响应读请求,这样被读取的相邻内容也同时被读出来,如果读是随机的,将会使CPU变慢,预取一般与软件进行配合以达到最高性能.

4) 内存一致性

write back会涉及内存一致性,涉及到一系列的问题:

  1. 多处理要系统更新cache时,一个处理器修改了cache的内容,第二个处理器将不能访问这个cache,直到这个cache的内容被写内存。在现代处理器中硬件已经做了精心的设计,确保这种事情不会发生,硬件负责保持cache在各个CPU之间一致.

  2. 外围硬件设备可以通过DMA(Direct Memory Access)访问内存,而不让处理器知道,也不会利用cache,这样在内存和cache之间就会出现不同步的情况。管理DMA的操作是操作系统的工作,比如设备驱动程序,它将保证内存与cache的一致性.

  3. 当在cache中的数据比内存中的数据老时,称为stale。如果软件初始化DMA,使设备和RAM之间传递数据,那么软件必须告诉 CPU,cache中的条目必须失效.

  4. 当在cache中的数据比内存中的数据新时,称为dirty。在设备驱动程序允许一个设备经DMA从内存读数据时,它必须确保所有的dirty 条目写进内存,也叫做flushing或sync cache

Cache与TLB

Cache与TLB本质上都是利用数据访问的局部性原理,就是把最常用的数据放在最快可以访问的地方。有所不同的是,Cache存放的是内存中的数据或者代码,而TLB存放的是页表项。

提到页表项,有必要简短介绍一下处理器的发展历史。最初的程序员直接对物理地址编程,自己去管理内存,这样不仅对程序员要求高,编程效率低,而且一旦程序出现问题也不方便进行调试。特别还出现了恶意程序,这对计算机系统危害实在太大,因而后来不同的体系架构推出了虚拟地址和分页的概念。

  • 虚拟地址是指程序员使用虚拟地址进行编程,不用关心物理内存的大小,即使自己的程序出现了问题也不会影响其他程序的运行和系统的稳定。
  • 分页是指把物理内存分成固定大小的块,按照页来进行分配和释放。一般常规页大小为4K(2^12)个字节,之后又因为一些需要,出现了大页,比如2M(2 ^21)个字节和1G(2 ^30)个字节的大小,我们后面会讲到为什么使用大页。
    处理器在寄存器收到虚拟地址之后,根据页表负责把虚拟地址转换成真正的物理地址。

具体而言:

  • Cache是用于缓存CPU常用的数据和指令的,分别称为dcache(data cache)和icache(instruction cache)。Cache通常有多级,比如L1 Cache、L2 Cache和L3 Cache,只有L1 Cache中是将指令和数据分开存放的,L2/L3 Cache中指令和数据通常是混合存放的。
  • TLB(Translation Lookaside Buffer)是用于缓存虚拟地址到物理地址的映射关系的,通常位于MMU中。在操作系统中,进程使用的虚拟地址需要转换成物理地址才能得到数据在物理内存中的真正存放位置,而内存管理单元MMU的一个功能就是进行虚拟地址到物理地址的转换。注意:多个虚拟地址可能会对应同一个物理地址。

Cache与TLB谁先访问?

通常来说,CPU发出的数据访问请求都是虚拟地址的,那CPU发出的数据请求是先到Cache中查找有无该虚拟地址对应的数据还是先利用MMU进行虚拟地址到物理地址转换然后再查找Cache呢?答案是上述2种情况都有可能,这取决于TLB与Cache的先后顺序。

  • 如果TLB介于 CPU 和 Cache之间,其实就是MMU介于CPU和Cache之间,则是先访问TLB后访问Cache且此时访问Cache使用的是物理地址,这种情况称为物理Cache。
  • 如果TLB介于 Cache和Memory之间,即MMU介于Cache与Memory之间,则是先访问Cache后访问TLB且此时访问Cache使用的是虚拟地址,这种情况称为逻辑Cache(注意:实际上此时可以同时向Cache发出请求,如果Cache命中则最好,如果Cache未命中此时MMU可以进行地址转换以便于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不命中的情况。

DDIO

服务器是如何处理从网络上来的数据?

  1. 当一个网络报文送到服务器的网卡时,网卡通过外部总线(比如 PCI总线)把数据和报文描述符送到内存。
  2. CPU从内存读取数据 到Cache进而到寄存器。
  3. 进行处理之后,再写回到Cache,并最终送到内存中。
  4. 最后,网卡读取内存数据,经过外部总线送到网卡内部,最终通过网络接口发送出去。

可以看出,对于一个数据报文,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

NUMA架构

https://blog.csdn.net/qq_15437629/article/details/77822040

DPDK中的Cache优化

前面介绍了一些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

以上是关于cache和内存的主要内容,如果未能解决你的问题,请参考以下文章

简述Cpu与cache.主存和外存的关系?

有一主存/Cache层次的存储器,其主要容量1Mb,Cache容量64Kb,每个数据块的大小16B若采用两路组相联映射方式

内存Cache直接映射、全相联映射和组相联映射

操作系统下cache的几个概念

关于计算机组成原理的Cache-主存地址映象问题

cache主存同时访问缺点