3.1 CPU硬件与缓存
8.1 CPU的物理核与逻辑核
相信绝大多数的同学都了解CPU核数,但通过 top 命令看到的核数并非真正的物理核数。例如 top 显示有24核,但实际物理核可能只有12个。这是因为 Intel 的超线程技术(Hyper-Threading Technology)让一个物理核虚拟出多个逻辑核。
基本概念:
- 物理CPU:主板上实际安装的CPU个数,可通过
physical id查看。 - 物理核:一个CPU内部集成的物理核心数,通过
core id查看。 - 逻辑核:超线程技术从物理核虚拟出的逻辑处理器,通过
processor序号标识。
实际查看CPU信息
在 Linux 下通过 cat /proc/cpuinfo 可查看详细CPU信息。
查看物理CPU数量:
# cat /proc/cpuinfo | grep "physical id" | sort | uniq
physical id : 0
physical id : 1说明该机器有2个物理CPU。
查看物理核数:
# cat /proc/cpuinfo | grep "cpu cores" | uniq
cpu cores : 6每个物理CPU有6个物理核,共计12个物理核。
查看逻辑CPU(处理器):
# cat /proc/cpuinfo | grep -E "core id|process|physical id"
processor : 0
physical id : 0
core id : 0
......
processor : 12
physical id : 0
core id : 0
......
processor : 23
physical id : 1
core id : 10processor 0 和 processor 12 的 physical id 和 core id 相同,说明它们来自同一个物理核,只是超线程虚拟出的两个逻辑核。总共24个逻辑核(processor 0~23)。
超线程的真相
超线程中的两个逻辑核实际上是共享同一物理核的L1和L2缓存,物理计算能力并未增加。相比实核,平均性能提升仅约20-30%。由于进程共享L1/L2,可能导致 cache miss 增加,实际性能甚至可能比不开超线程更差。因此操作系统看到的24核只是一个“假象”。
8.2 CPU之高速缓存
CPU缓存是现代CPU设计的核心任务之一,对程序性能影响极大。缓存的发展源于CPU与内存速度差距不断扩大。早期CPU无缓存,后逐步演化出L1、L2、L3三级缓存,并全部集成到CPU芯片内。
Intel CPU缓存体系结构
- L1:最接近CPU,速度最快,容量最小。现代CPU通常将L1分为 Data Cache(数据缓存)和 Instruction Cache(指令缓存),因为代码和数据更新策略不同。
- L2:速度慢于L1,容量更大,通常每个物理核拥有独立的L2。
- L3:速度最慢,容量最大,通常整个物理CPU共享一个L3。
实际查看CPU缓存
Linux 内核通过 CPUFreq 系统 提供 /sys/devices/system/cpu/ 下的接口查看缓存详细信息。
查看L1一级缓存:
# cd /sys/devices/system/cpu/
# cat cpu0/cache/index0/level
1
# cat cpu0/cache/index0/size
32K
# cat cpu0/cache/index0/type
Data
# cat cpu0/cache/index0/shared_cpu_list
0,12
# cat cpu0/cache/index1/level
1
# cat cpu0/cache/index1/size
32K
# cat cpu0/cache/index1/type
Instruction
# cat cpu0/cache/index1/shared_cpu_list
0,12index0 和 index1 都是L1,分别是 Data 和 Instruction 缓存。shared_cpu_list 显示 0,12,表示这两个逻辑核(同一物理核)共享此L1缓存。该机器共有12个Data L1和12个Instruction L1,大小各32K。
查看L2二级缓存:
# cat cpu0/cache/index2/size
256K
# cat cpu0/cache/index2/type
Unified
# cat cpu0/cache/index2/shared_cpu_list
0,12L2大小为256K,不分数据和指令,同样每两个逻辑核共享一个L2(共12个)。
查看L3三级缓存:
# cat cpu0/cache/index3/size
12288K
# cat cpu0/cache/index3/type
Unified
# cat cpu0/cache/index3/shared_cpu_list
0-5,12-17
# cat cpu6/cache/index3/shared_cpu_list
6-11,18-23L3大小为12M,该机器只有两个L3,每个物理CPU一个。第0-5、12-17号逻辑核共享第一个L3;其余共享第二个。
查看CPU缓存的更多方式
dmidecode -t cache也能查看缓存信息- Windows 下可用命令
wmic cpu get L2CacheSize,L3CacheSize
扩展:Cache Line
Cache Line 是本级缓存向下一层取数据的基本单位。可通过以下方式查看:
# cat cpu0/cache/index0/coherency_line_size
64
# cat cpu0/cache/index1/coherency_line_size
64
# cat cpu0/cache/index2/coherency_line_size
64
# cat cpu0/cache/index3/coherency_line_size
64L1、L2、L3 的 Cache Line 大小均为 64字节。CPU 每次从内存获取数据时(即使只取1个字节),也会一次性读取一个 Cache Line 并缓存到各级缓存中。这一机制对性能优化至关重要(参见 7.2 PHP7内存性能优化思想精髓)。
8.3 CPU之TLB缓存
虚拟内存与页表回顾
操作系统为每个进程提供独立的虚拟地址空间。虚拟地址到物理地址的转换通过页表机制实现。Linux 下页大小默认为4KB:
# getconf PAGE_SIZE
4096页表级数:64位系统采用四级页表(PGD / PUD / PMD / PTE),支持256T的进程地址空间。但这也意味着一次虚拟地址翻译可能需要多次内存访问(最坏情况下4次页表 + 1次数据访问)。
TLB的诞生
TLB(Translation Lookaside Buffer) 是CPU内部用于缓存页表的高速缓存,访问速度极快,与寄存器相当,比L1还快。有了TLB后,CPU访问内存的流程变为:
- CPU产生虚拟地址
- MMU从TLB获取页表,翻译成物理地址
- MMU将物理地址发送给L1/L2/L3/内存
- 返回数据给CPU
如果TLB命中,地址翻译的时间开销几乎可以忽略;若TLB miss,则需多次内存IO,代价极高。
查看TLB命中率
使用 perf 工具可以查看进程的TLB命中情况:
# perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses -p $PID
Performance counter stats for process id '21047':
627,809 dTLB-loads
8,566 dTLB-load-misses # 1.36% of all dTLB cache hits
2,001,294 iTLB-loads
3,826 iTLB-load-misses # 0.19% of all iTLB cache hits其中 dTLB 为数据TLB,iTLB 为指令TLB。如果miss率较高,可考虑启用 大内存页(Huge Pages)来减少页表项数量,降低TLB miss。默认大内存页未开启。
TLB在Linux中暂无类似sysfs的直接查看命令,但可通过
perf、numactl等间接了解。
本部分对应原文第8章第1-3节,后续内容(8.4 CPU性能优化之辅助分支预测)将在下一段输出。
扩展知识:Cache Line
我们前面只介绍了各个级别的缓存,但是这里面有个很重要的概念就是 Cache Line,即本级缓存向下一层取数据时的基本单位。可以通过如下方式查看:
# cd /sys/devices/system/cpu/
# cat cpu0/cache/index0/coherency_line_size
64
# cat cpu0/cache/index1/coherency_line_size
64
# cat cpu0/cache/index2/coherency_line_size
64
# cat cpu0/cache/index3/coherency_line_size
64可以看到 L1、L2、L3 的 Cache Line 大小都是 64 字节(注意是字节)。也就是说,每次 CPU 从内存获取数据时,都以该单位进行,哪怕你只取一个 bit,CPU 也会给你取一个 Cache Line 然后放到各级缓存里存起来。请牢牢记住这个概念,后续文章中会用到。
8.3 CPU 之 TLB 缓存
虚拟内存
在用户的视角里,每个进程都有自己独立的地址空间,A 进程的 4GB 和 B 进程的 4GB 是完全独立不相关的,他们看到的都是操作系统虚拟出来的地址空间。但是,虚拟地址最终还是要落在实际内存的物理地址上进行操作的。操作系统就会通过页表的机制来实现进程的虚拟地址到物理地址的翻译工作。其中每一页的大小都是固定的。这一段我不想介绍得太过于详细,对这个概念不熟悉的同学回去翻一下操作系统的教材。
页表管理有两个关键点,分别是页面大小和页表级数:
-
页面大小
在 Linux 下,我们通过如下命令可以查看到当前操作系统的页大小:# getconf PAGE_SIZE 4096可以看到当前我的 Linux 机器的页表是 4KB 的大小。
-
页表级数
页表级数越少,虚拟地址到物理地址的映射会很快,但是需要管理的页表项会很多,能支持的地址空间也有限。
相反页表级数越多,需要的存储的页表数据就会越少,而且能支持到比较大的地址空间,但是虚拟地址到物理地址的映射就会越慢。
32 位系统的虚拟内存实现:二级页表
为了帮助大家回忆这段知识,我举个例子。如果想支持 32 位的操作系统下的 4GB 进程虚拟地址空间,假设页表大小为 4K,则共有 2^20 个页面。如果采用速度最快的 1 级页表,对应则需要 2^20 个页表项。一个页表项假如 4 字节,那么一个进程就需要(1048576×4=)4M 的内存来存页表项。
如果采用 2 级页表,则创建进程时只需要有一个页目录就可以了,占用 (1024×4)=4KB 的内存。剩下的二级页表项只有用到的时候才会再去申请。
64 位系统的虚拟内存实现:四级页表
现在的操作系统需要支持的可是 48 位地址空间(理论上可以支持 64 位,但其实现在只支持到了 48 位,也足够用了),而且要支持成百上千的进程,如果不采用分级页表的方式,则创建进程时就需要为其维护一个 2^36 个页表项(64 位 Linux 目前只使用了地址中的 48 位,在这里面,最后 12 位都是页内地址,只有前 36 位才是用来寻找页表的),2^36 × 4Byte = 32GB,这个更不能忍了。也必须和 32 位系统一样,进一步提高页表的级数。
Linux 在 v2.6.11 以后,最终采用的方案是 4 级页表,分别是:
- PGD:page Global directory (47-39),页全局目录
- PUD:Page Upper Directory (38-30),页上级目录
- PMD:page middle directory (29-21),页中间目录
- PTE:page table entry (20-12),页表项
这样,一个 64 位的虚拟空间,初始创建的时候只需要维护一个 2^9 大小的一个页全局目录就够了,现在的页表数据结构被扩展到了 8byte。这个页全局目录仅仅需要 (2^9 × 8=)4K,剩下的中间页目录、页表项只需要在使用的时候再分配就好了。Linux 就是通过这种方式支持起 (2^48 =)256T 的进程地址空间的。
页表带来的问题
上面终于费劲扒了半天 Linux 虚拟内存的实现,我终于可以开始说我想说的重点了。
虽然创建一个支持 256T 的地址空间的进程在初始的时候只需要 4K 的页全局目录,但是,这也带来了额外的问题:页表是存在内存里的。那就是一次内存 IO 光是虚拟地址到物理地址的转换就要去内存查 4 次页表,再算上真正的内存访问,最坏情况下需要 5 次内存 IO 才能获取一个内存数据!
TLB 应运而生
和 CPU 的 L1、L2、L3 的缓存思想一致,既然进行地址转换需要的内存 IO 次数多,且耗时。那么干脆就在 CPU 里把页表尽可能地 cache 起来不就行了么,所以就有了 TLB(Translation Lookaside Buffer),专门用于改进虚拟地址到物理地址转换速度的缓存。其访问速度非常快,和寄存器相当,比 L1 访问还快。
我本来想实际看一下 TLB 的信息,但翻遍了 Linux 的各种命令,也没有找到像 sysfs 这么方便查看 L1、L2、L3 大小的方法。下面仅提供下图供大家参考吧! (谁要是找到了查看 TLB 的命令,别忘了分享给飞哥啊,谢谢!)
TIP
TLB 是 CPU 内部的一个硬件单元,不同架构的 TLB 大小和组织方式不同,通常无法通过普通文件接口直接查看。可以通过
cpuid指令或查阅 CPU 手册获得。
有了 TLB 之后,CPU 访问某个虚拟内存地址的过程如下:
- CPU 产生一个虚拟地址
- MMU 从 TLB 中获取页表,翻译成物理地址
- MMU 把物理地址发送给 L1/L2/L3/内存
- L1/L2/L3/内存将地址对应数据返回给 CPU
由于第 2 步是类似于寄存器的访问速度,所以如果 TLB 能命中,则虚拟地址到物理地址的时间开销几乎可以忽略。如果想了解 TLB 更详细的工作机制,请参考《深入理解计算机系统 - 第9章 虚拟内存》。
如何查看 TLB 命中率
既然 TLB 缓存命中很重要,那么有什么工具能够查看你的系统里的命中率呢?还真有:
# perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses -p $PID
Performance counter stats for process id '21047':
627,809 dTLB-loads
8,566 dTLB-load-misses # 1.36% of all dTLB cache hits
2,001,294 iTLB-loads
3,826 iTLB-load-misses # 0.19% of all iTLB cache hitsNOTE
因为 TLB 并不是很大(通常只有几 KB),而且现在逻辑核又造成会有两个进程来共享,所以可能会有 cache miss 的情况出现。而且一旦 TLB miss 造成的后果可比物理地址 cache miss 后果要严重一些,最多可能需要进行 5 次内存 IO 才行。建议你先用上面的
perf工具查看一下你的程序的 TLB 的 miss 情况,如果确实不命中率很高,那么 Linux 允许你使用 大内存页,很多大牛包括 PHP7 作者鸟哥也这样建议。这样将会大大减少页表项的数量,所以自然也会降低 TLB cache miss 率。所要承担的代价就是会造成一定程度的内存浪费。在 Linux 里,大内存页默认是不开启的。