一、CPU Cache
1、CPU Cache简介
CPU Cache是位于CPU与内存之间的临时存储器,容量比内存小但交换速度却比内存要快得多。Cache的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,会使CPU花费很长时间等待数据到来或把数据写入内存。在Cache中的数据是内存中的一小部分,但是CPU短时间内即将访问的,当CPU调用大量数据时,就可避开内存直接从Cache中调用,从而加快读取速度。Cache对CPU的性能影响很大,主要是因为CPU的数据交换顺序和CPU与Cache间的带宽引起的。
Cache工作原理是当CPU要读取一个数据时,首先从Cache中查找,如果找到就立即读取并送给CPU处理;如果没有找到,就用相对慢的速度从内存中读取并送给CPU处理,同时把数据所在的数据块调入Cache中,可以使得以后对整块数据的读取都从Cache中进行,不必再调用内存。
CPU读取Cache的命中率非常高(大多数CPU可达90%左右),大大节省了CPU直接读取内存的时间,也使CPU读取数据时基本无需等待。
按照数据读取顺序和与CPU结合的紧密程度,CPU Cache可以分为L1 Cache、L2 Cache、L3 Cache,每一级Cache中所储存的全部数据都是下一级Cache的一部分,三种Cache的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当CPU要读取一个数据时,首先从L1 Cache中查找,如果没有找到再从L2 Cache中查找,如果还是没有就从L3 Cache或内存中查找。通常每级Cache的命中率大概都在80%左右,即全部数据量的80%都可以在L1 Cache中找到,只剩下20%的总数据量才需要从L2 Cache、L3 Cache或内存中读取,因此L1 Cache是CPU Cache架构中最为重要的部分。
2、CPU Cache结构
Intel CPU中,8086、80286时代CPU是没有Cache的,因为当时CPU和内存速度差异并不大,CPU直接访问内存。但80386开始,CPU速度要远大于内存访问速度,第一次出现了Cache,而且最早的Cache并没有放在CPU模块,而放在主板上。CPU Cache的材质SRAM比内存DRAM贵很多,大小以MB为单位,因此现代计算机在CPU和内存之间引入了高速Cache,作为CPU和内存之间的通道。经过长期发展演化,逐步演化出了L1、L2、L3三级缓存结构,而且都集成到CPU芯片。
L1 Cache最接近于CPU,速度最快,但容量最小。现代CPU的L1 Cache会分成两个:数据Cache和指令Cache,指令和数据的更新策略并不相同,而且因为CISC的变长指令,指令Cache要做特殊优化。每个CPU核都有自己独立的L1 Cache和L2 Cache,但L3 Cache通常是整颗CPU共享。
3、CPU Cache查看
Linux内核开发者定义了CPUFreq系统查看CPU详细信息,/sys/devices/system/cpu目录保存了CPU详细信息。
L1 Cache查看
L2 Cache查看
L3 Cache查看
CPU Cache查看命令如下:
- dmidecode -t cache
-
- getconf -a | grep CACHE
CPU只有3级Cache,L4为0,L1 Cache数据缓存和指令缓存分别为32KB,L2 Cache为256KB,L3 Cache为3MB,Cache Line为64字节。ASSOC表示主存地址映射到缓存的策略,L1、L2是8路组相联,L3是12路组相联。
L1 Cache中数据Cache和指令Cache是物理CPU核共享的,因此,超线程技术虚拟出来的逻辑CPU核CPU0和CPU1共享L1 Cache。
4、Cache Line
Cache Line是CPU Cache中的最小缓存单位,是本级Cache向下一层取数据时的基本单位。目前主流的CPU Cache的Cache Line大小都是64Bytes,即当程序需要从内存中读取一个字节的时候,相邻的63字节同时会从内存中加载到CPU Cache中,当CPU访问相邻的数据的时候,并不会从内存中读取数据,而从CPU Cache中即可访问到数据,提高了速度。
L1、L2、L3的Cache Line大小都是64字节,可以通过如下查看:
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
CPU寄存器、Cache、内存的性能指标参考
CPU要访问的数据在Cache中有缓存,称为hit(命中),反之则称为miss(缺失)。
当CPU试图从某地址load数据时,首先从L1 Cache中查询是否命中,如果命中则把数据返回给CPU。如果L1 Cache缺失,则继续从L2 Cache中查找。当L2 Cache命中时,数据会返回给L1 Cache以及CPU。如果L2 Cache、L3 Cache也缺失,需要从主存中load数据,将数据返回给L3 Cache、L2 Cache、L1 Cache及CPU。
5、多核Cache一致性
每个CPU核都有一个私有的L1 Cache,对于多核CPU,不同CPU核之间的L1 Cache需要保证一致性。
(1)Bus Snooping Protocol
当某个CPU核修改自己私有Cache时,硬件会广播通知到总线上其它所有CPU核。每个CPU核都有特殊的硬件监听广播事件,并检查是否有相同的数据被缓存在自己的Cache。
Bus Snooping需要时刻监听总线上的一切活动,在一定程度上加重了总线负载,增加了读写延迟。
(2)MESI协议
MESI协议是一种使用状态机机制降低带宽压力来维护多核Cache一致性的协议。
MESI协议名称来源于cache line的4种状态Modified、Exclusive、Shared、Invalid的简写。当cache line状态是Modified或者Exclusive状态时,修改其数据不需要发送消息给其它CPU,在一定程度上减轻了带宽压力。
多核Cache一致性由硬件保证,现代CPU硬件采用的一致性协议通常是MESI协议的变种,如ARM64架构采用MOESI协议。
二、TLB
1、MMU简介
MMU(Memory Management Unit)是一种通过段机制和页机制将虚拟地址转换为物理地址的硬件电路,通常集成在CPU芯片内。
MMU通过建立页表,完成虚拟地址到物理地址的转换。当需要访问内存中的一个数据,通过数据的虚拟地址查找页表,一旦在页表命中,就通过找到的物理地址寻址到内存中的数据。如果页表中没有命中,表示页表中没有建立数据虚拟地址到物理地址的映射,通过缺页异常,建立数据的页表映射项。
对于频繁使用的不变数据,每次都回进行查找数据和相应页表项,为了加快页表查找速度,减少不必要的重复,引入TLB。
2、TLB简介
TLB( Translation Look- aside Buffer)专门用于缓存内存中的页表项,通常集成在MMU内部。TLB是一个很小的Cache,TLB表项( TLB entry)数量比较少,每个TLB表项包含一个页面的相关信息,例如有效位、虚拟页号、修改位、物理页帧号等。当处理器要访问一个虚拟地址时,首先会在TLB中查询。如果TLB表项中没有命中,就需要访问页表来计算出相应的物理地址。如果TLB表项中命中,直接从TLB表项中获取物理地址。
TLB内部存放的基本单位是TLB表项,TLB容量越大,所能存放的TLB表项就越多,TLB命中率就越高。但是TLB容量是有限的,目前Linux内核默认采用4KB大小的小页面,如果一个程序使用512个小页面,即2MB大小,那么至少需要512个TLB表项才能保证不会出现TLB Miss。但如果使用2MB大小的大页,只需要一个TLB表项就可以保证不会出现TLB Miss。对于消耗内存以GB为单位的大型应用程序,还可以使用以1GB为单位的大页,从而减少TLB Miss的情况。
TLB本质是页表的Cache,TLB缓存了最近使用的数据的页表项(虚拟地址到物理地址的映射)。TLB的出现是为了加快访问内存数据的速度,减少重复的页表查找,不是必须的,但TLB可以提高访问内存数据的速度。
3、TLB原理
当CPU要访问一个虚拟地址/线性地址时,CPU会首先根据虚拟地址的高20位(X86架构)在TLB中查找。如果是表中没有命中,需要通过访问慢速RAM中的页表计算出相应的物理地址。同时,物理地址被存放在一个TLB表项中,以后对同一线性地址的访问,直接从TLB表项中获取物理地址即可。
TLB结构如下:
如果一个需要访问内存中的一个数据,给定数据的虚拟地址,查询TLB,发现有(hit),直接得到物理地址,在内存根据物理地址读取数据。如果TLB没有命中(miss),只能通过页表来查找。
4、TLB映射方式
TLB内部存放的基本单位是TLB表项,对应着RAM中存放的页表条目。页表条目的大小固定不变的,所以TLB容量越大,所能存放的页表条目越多,TLB命中越大。但TLB容量毕竟是有限的,因此RAM页表和TLB表项无法做到一一对应。
(1)全相连
全相连方式,TLB cache中的表项和线性地址之间没有任何关系,一个TLB表项可以和任意线性地址的页表项关联。全相连方式关联方式使得TLB表项空间的利用率最大,但延迟也可能很大,因为每次CPU请求,TLB硬件都把线性地址和TLB表项逐一比较,直到TLB命中或者所有TLB表项比较完成。随着CPU Cache越来越大,需要比较大量TLB表项,所以只适合小容量TLB。
(2)直接匹配
每一个线性地址块都可通过模运算对应到唯一的TLB表项,只需进行一次比较,降低了TLB内比较延迟,但产生冲突的很大,导致TLB miss的发生,降低了命中率。
(3)组相连
为了解决全相连内部比较效率低和直接匹配的冲突,引入了组相连。组相连把所有的TLB表项分成多个组,每个线性地址块对应的不再是一个TLB表项,而是一个TLB表项组。CPU做地址转换时,首先计算线性地址块对应哪个TLB表项组,然后在TLB表项组按顺序比对。按照组长度,可以分为2路、4路、8路。
经过长期工程实践,发现8路组相连是一个性能分界点。8路组相连的命中率和全相连命中率相当,超过8路,组内对比延迟带来的缺点会超过命中率提高的优势。
三、超线程技术
1、超线程技术简介
超线程(Hyperthreading)技术在一个物理核上模拟两个逻辑核,两个逻辑核具有各自独立的寄存器(eax、ebx、ecx、msr等)和APIC,但会共享使用物理核的执行资源,包括执行引擎、L1/L2 Cache、TLB和系统总线等等。
2、超线程对性能影响
超线程技术仅仅是在一个物理核心上使用了两个物理任务描述符,物理计算能力并没有增加。采用多worker设计应用,在超线程的帮助下,两个被调度到同一核心不同超线程的worker,通过共享Cache、TLB,大幅降低了任务切换开销。在某个worker不忙的时候,超线程允许其它的worker也能使用物理计算资源,有助于提升物理核整体的吞吐量。但由于超线程对物理核执行资源的争抢,业务的执行时延也会相应增加。
开启超线程后,物理核总计算能力是否提升以及提升的幅度和业务模型相关,平均提升在20%-30%左右
当超线程相互竞争时,超线程的计算能力相比不开超线程时的物理核会下降30%左右
3、超线程应用
超线程开启或关闭取决于应用模型,对于时延敏感型任务,在节点负载过高时会引发超线程竞争,任务执行时长会显著增加,应该关闭超线程;对于后台计算型任务,建议开启超线程提高整个节点的吞吐量。
四、NUMA技术
1、NUMA简介
NUMA架构出现前,CPU朝着高频率方向发展收到物理极限的挑战,转而向着多核心的方向发展。由于所有CPU Core都通过共享一个北桥来读取内存,随着CPU核数增加,北桥在响应时间上的性能瓶颈越来越明显。因此,硬件设计师把内存控制器从北桥中拆分出来,平分到每个CPU Core上,即NUMA架构。
NUMA(Non-Uniform Memory Access)起源于AMD Opteron微架构,同时被Intel Nehalem采用。
NUMA架构将整台服务器分为若干个节点,每个节点上有单独的CPU和内存。只有当CPU访问自身节点内存对应的物理地址时,才会有较短的响应时间(后称Local Access)。而如果需要访问其他节点的内存数据时,需要通过inter-connect通道访问,响应时间变慢(后称Remote Access)。所以NUMA(Non-Uniform Memory Access)就此得名。
在Node内部,使用IMC Bus进行不同核心间的通信;不同的Node 间通过QPI(Quick Path Interconnect)进行通信。
一个Socket(内存插槽)对应一个Node,QPI的延迟要高于IMC Bus,CPU访问内存存在远近(remote/local)。
2、NUMA缺陷
NUMA架构的内存分配策略对于进程或线程是不公平的。RHEL Linux中,localalloc是默认的NUMA内存分配策略,即从当前node上请求分配内存,会导致资源独占程序很容易将某个node的内存用尽。而当某个node的内存耗尽时,Linux刚好将CPU node分配给某个需要消耗大量内存的进程(或线程),此时将产生swap。
3、NUMA应用
(1)BIOS关闭NUMA
BIOS的Memory Setting菜单找到Node Interleaving项,设置为Disabled表示启用NUMA,非一致访问方式访问,是默认配置;设置为Enabled表示关闭NUMA,采用SMP方式启用内存交错模式。
(2)软件层次关闭NUMA
启动应用前使用numactl –interleave=all修改NUMA策略即可。
如果应用会占用大规模内存,应该选择关闭NUMA Node限制(或从硬件关闭NUMA);如果应用并不占用大内存,而要求更快的程序运行时间,应该选择限制只访问本NUMA Node方法来进行处理。
检查BIOS是否开启NUMA
grep -i numa /var/log/dmesg
检查当前系统的NUMA节点
numactl --hardware
lscpu
查看内存数据
numastat
当发现numa_miss数值比较高时,说明需要对分配策略进行调整,如将进程绑定到指定CPU上,从而提高内存命中率。
在开启NUMA支持的Linux中,Kernel不会将任务内存从一个NUMA node迁移到另一个NUMA node。
进程一旦启动,所在NUMA node就不会被迁移,为了尽可能的优化性能,在正常的调度之中,CPU的core也会尽可能的使用可以local访问的本地core,在进程的整个生命周期之中,NUMA node保持不变。
一旦当某个NUMA node负载超出另一个node一个阈值(默认25%),则认为需要在此node上减少负载,不同的NUMA结构和不同的负载状况,系统见给予一个延时任务的迁移——类似于漏杯算法。在这种情况下将会产生内存的remote访问。
NUMA node之间有不同的拓扑结构,不同node间的访问会有一个距离概念,可以使用numactl -H进行查看。
4、NUMA Node绑定
numactl [--interleave nodes] [--preferred node] [--membind nodes] [--cpunodebind nodes] [--physcpubind cpus] [--localalloc] command
--interleave=nodes, -i nodes用于设定内存的交织分配模式,系统在为多个节点分配内存空间时,将会以轮询分发的方式被分配给多个节点。如果在当前众多的交织分配内存节点中的目标节点无法正确的分配内存空间,内存空间将会由其它的节点来分配。
--membind=nodes, -m nodes从指定节点中分配内存空间,如果节点内存空间不足,分配操作将会失败。
--cpunodebind=nodes, -N nodes用于绑定进程到CPU Node。
--physcpubind, -C cpus用于把进程绑定到CPU核心上。
--localalloc , -l 启动进程,并在当前CPU节点分配内存。
--preferred=node用于指定优先分配内存空间的节点,如果无法在节点分配空间,会从其它节点分配。
numactl --cpubind=0 --membind=0 python param
numactl --show显示当前NUMA机制
numactl --hardware显示当前系统中有多少个可用的节点。
numactl [--huge] [--offset offset] [--shmmode shmmode] [--length length] [--strict]
创建共享内存段