《开发内功修炼-内存篇》学习笔记
目录
- CPU物理结构
- CPU缓存
- 进程/线程切换的开销
- 软中断的开销
- 系统调用的开销
- 函数调用的开销
参考/来源
- 《开发内功修炼》微信公众号
CPU物理结构
CPU核数
物理CPU:主板上真正安装的CPU的个数,
物理核:一个CPU会集成多个物理核心
逻辑核:超线程技术可以把一个物理核虚拟出来多个逻辑核
超线程里的2个逻辑核实际上是在一个物理核上运行的,模拟双核心运作,共享该物理核的L1和L2缓存。物理计算能力并没有增加,超线程技术只有在多任务的时候才能提升机器核整体的吞吐量。而且据Intel官方介绍,相比实核,平均性能提升只有20-30%30%左右。
Linux的CPU
top
命令其实我们通过top命令看到的CPU核是逻辑核
更详细信息
在linux系统下,通过查看
/proc/cpuinfo
可以看到CPU更为详细的信息。我们通过physical id 可以看到真正的物理CPU的个数,如下:#cat /proc/cpuinfo | grep "physical id" | sort | uniq physical id : 0 physical id : 1
可以看出,该实机有两个物理CPU。我们继续查看物理核,通过cpu cores可以看到每个CPU有几个物理核。
#cat /proc/cpuinfo| grep "cpu cores"| uniq cpu cores : 6
cpu cores显示为6表示每个cpu有6个物理核心,因为有2个物理CPU,所以该机器总共有12个物理核。当然该命令也可以查看逻辑核,那就是grep显示结果中的processor。
#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 : 10
processor就是逻辑核的序号,可以看出该机器总共有24个逻辑核。大家注意看processor 0和processor 12的physical id、core id都是一样的,也就说他们他们也处在同一个物理核上。但是他们的processor编号却不一样,一个是0,一个是12。这就是说,这两个逻辑核实际上是一个物理核虚拟出来的而已。
CPU缓存
L1\L2\L3缓存
CPU越来越快,现在CPU的速度比内存要快百倍以上,所以就逐步演化出了L1、L2、L3三级缓存结构,而且都集成到的CPU芯片里,以进一步提高访问速度。
现代Intel的CPU架构的基本结构:
越往下,速度越慢,容量越大
L1最接近于CPU,速度也最快,但是容量最小。一般现代CPU的L1会分成两个,一个用来cache data,一个用来cache code,这是因为code和data的更新策略并不相同,而且因为CISC的变长指令,code cache要做特殊优化。一般每个核都有自己独立的data L1和code L1。
L2一般也可以做到每个核一个独立的
但是L3一般就是整颗CPU共享的了。
UEFIBlog里提供了一个比较好的物理解剖图,比较好地展示了出来:
查看Linux的三级缓存
L1一级缓存查看:
# 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,12
- 从上面的level接口可以看出index0和index1都是一级缓存,只不过一个是Data数据缓存,一个是Instruction也就是代码缓存。
- shared_cpu_list显示有共享?我们这里看到的cpu0并不是物理Core,而是逻辑核,都是超线程技术虚拟出来的。实际上cpu0和cpu12是属于一个物理Core,所以每个Data L1和Instruction是这两个逻辑核共享的。
L2二级缓存查看:
# cat cpu0/cache/index2/size
256K
# cat cpu0/cache/index2/type
Unified
# cat cpu0/cache/index2/shared_cpu_list
0,12
二级缓存要比一级缓存大不少,有256K,但是不分Data和Instruction。另外L2和L1一样,也是总共有12个,每两个逻辑核共享一个L2。
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-23
Cache Line
Cache Line是本级缓存向下一层取数据时的基本单位
可以看到L1、L2、L3的Cache Line大小都是64字节(注意是字节,可以在相关配置文件查看)。就是说每次cpu从内存获取数据的时候,都是以该单位来进行的,哪怕你只取一个bit,CPU也是给你取一个Cache Line然后放到各级缓存里存起来。
TLB
和CPU的L1、L2、L3的缓存思想一致,既然进行地址转换需要的内存IO次数多,且耗时。那么干脆就在CPU里把页表尽可能地cache起来不就行了么,所以就有了TLB(Translation Lookaside Buffer),专门用于改进虚拟地址到物理地址转换速度的缓存。其访问速度非常快,和寄存器相当,比L1访问还快。
有了TLB之后,CPU访问某个虚拟内存地址的过程如下
CPU产生一个虚拟地址
MMU从TLB中获取页表,翻译成物理地址
MMU把物理地址发送给L1/L2/L3/内存
L1/L2/L3/内存将地址对应数据返回给CPU
TLB并不是很大,只有4k,而且现在逻辑核又造成会有两个进程来共享。所以可能会有cache miss的情况出现。而且一旦TLB miss造成的后果可比物理地址cache miss后果要严重一些,最多可能需要进行5次内存IO才行。
进程/线程切换的开销
上下文切换
上下文切换
在进程A切换到进程B的过程中,先保存A进程的上下文,以便于等A恢复运行的时候,能够知道A进程的下一条指令是啥。然后将要运行的B进程的上下文恢复到寄存器中。这个过程被称为上下文切换。
测试开销的结果
每次执行的时间会有差异,多次运行后平均每次上下文切换耗时3.5us左右。当然了这个数字因机器而异,而且建议在实机上测试。
前文我们测试系统调用的时候,最低值是200ns。可见,上下文切换开销要比系统调用的开销要大。系统调用只是在进程内将用户态切换到内核态,然后再切回来,而上下文切换可是直接从进程A切换到了进程B。显然这个上下文切换需要完成的工作量更大。
开销分析
开销分成两种,一种是直接开销、一种是间接开销。
直接开销就是在切换时,cpu必须做的事情,包括:
1、切换页表全局目录
2、切换内核态堆栈
3、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
- ip(instruction pointer):指向当前执行指令的下一条指令
- bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
- sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
- cr3:页目录基址寄存器,保存页目录表的物理地址
- ……
4、刷新TLB
5、系统调度器的代码执行
间接开销主要指的是虽然切换到一个新进程后,由于各种缓存并不热,速度运行会慢一些。如果进程始终都在一个CPU上调度还好一些,如果跨CPU的话,之前热起来的TLB、L1、L2、L3因为运行的进程已经变了,所以以局部性原理cache起来的代码、数据也都没有用了,导致新进程穿透到内存的IO会变多。其实我们上面的实验并没有很好地测量到这种情况,所以实际的上下文切换开销可能比3.5us要大。
相关命令
# vmstat 1 procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 595504 5724 190884 0 0 295 297 0 0 14 6 75 0 4 5 0 0 593016 5732 193288 0 0 0 92 19889 29104 20 6 67 0 7 3 0 0 591292 5732 195476 0 0 0 0 20151 28487 20 6 66 0 8 4 0 0 589296 5732 196800 0 0 116 384 19326 27693 20 7 67 0 7 4 0 0 586956 5740 199496 0 0 216 24 18321 24018 22 8 62 0 8
cs列表示的就是在1s内系统发生的上下文切换次数
# sar -w 1 proc/s Total number of tasks created per second. cswch/s Total number of context switches per second. 11:19:20 AM proc/s cswch/s 11:19:21 AM 110.28 23468.22 11:19:22 AM 128.85 33910.58 11:19:23 AM 47.52 40733.66 11:19:24 AM 35.85 30972.64 11:19:25 AM 47.62 24951.43 11:19:26 AM 47.52 42950.50
协程
在网络服务器环境下,为了避免频繁的上下文切换,有一种异步非阻塞的开发模型。那就是用一个进程或线程去接收一大堆用户的请求,然后通过IO多路复用的方式来提高性能(进程或线程不阻塞,省去了上下文切换的开销)。
在应用层,不需要进程/线程上下文切换的“线程”,即为协程。用协程去处理高并发的应用场景,既能够符合进程涉及的初衷,让开发者们用人类正常的线性的思维去处理自己的业务,也同样能够省去昂贵的进程/线程上下文切换的开销。因此可以说,协程就是Linux处理海量请求应用场景里的进程模型的一个很好的的补丁。
协程切换开销
平均每次协程切换的开销是(655035993-415197171)/2000000=120ns。相对于前面文章测得的进程切换开销大约3.5us,大约是其的三十分之一。比系统调用的造成的开销还要低。
内存开销
在空间上,协程初始化创建的时候为其分配的栈有2KB。而线程栈要比这个数字大的多,可以通过ulimit 命令查看,一般都在几兆,作者的机器上是10M。如果对每个用户创建一个协程去处理,100万并发用户请求只需要2G内存就够了,而如果用线程模型则需要10T。
无论是空间还是时间性能都比进程(线程)好这么多,那么Linus为啥不把它在操作系统里实现了多好?操作系统为了实现实时性更好的目的,对一些优先级比较高的进程是会抢占其它进程的CPU的。而协程无法实现这一点,还得依赖于挡前使用CPU的协程主动释放,于操作系统的实现目的不相吻合。所以协程的高效是以牺牲可抢占性为代价的。
软中断的开销
现代的Linux发明了软件中断,配合硬中断来处理网络IO。硬中断你可以理解只是个收包的,把包收取回来放到“家里”就完事,很快就能完成,这样不耽误CPU响应其它外部高优先级的中断。而软中断优先级较低,负责将包进行各种处理,完成从驱动层、到网络协议栈,最终把处理出来的数据放到socker的接收buffer中。软中断消耗的CPU周期相对比硬中断要多不少。
软中断和系统调用一样,都是CPU停止掉当前用户态上下文,保存工作现场,然后陷入到内核态继续工作。二者的唯一区别是系统调用是切换到同进程的内核态上下文,而软中断是则是切换到了另外一个内核进程ksoftirqd上。
1) 查看软中断总耗时
首先用top命令可以看出每个核上软中断的开销占比,是在si列
top
- 19:51:24 up 78 days, 7:53, 2 users, load average: 1.30, 1.35, 1.35Tasks: 923 total, 2 running, 921 sleeping, 0 stopped, 0 zombieCpu(s): 7.1%us, 1.4%sy, 0.0%ni, 90.1%id, 0.1%wa, 0.2%hi, 1.2%si, 0.0%stMem: 65872372k total, 64711668k used, 1160704k free, 339384k buffersSwap: 0k total, 0k used, 0k free, 55542632k cached
CPU大约花费了1.2%的时钟周期在软中断上,也就是说每个核要花费12ms。
2)查看软中断次数
再用vmstat命令可以看到软中断的次数
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu------
r b swpd free buff cache si so bi bo in cs us sy id wa st1
0 0 1231716 339244 55474204 0 0 6 496 0 0 7 3 90 0 0 2
0 0 1231352 339244 55474204 0 0 0 128 57402 24593 5 2 92 0 0 2
0 0 1230988 339244 55474528 0 0 0 140 55267 24213 5 2 93 0 0 2
0 0 1230988 339244 55474528 0 0 0 332 56328 23672 5 2 93 0 0
每秒大约有56000次左右的软中断(该机器上是web服务,网络IO密集型的机器,其它中断可以忽略不计)。
3)计算每次软中断的耗时
该机器是16核的物理实机,故可以得出每个软中断需要的CPU时间是=12ms/(56000/16)次=3.428us
从实验数据来看,一次软中断CPU开销大约3.4us左右
这个时间里其实包含两部分,一是上下文切换开销,二是软中断内核执行开销。其中上下文切换和系统调用、进程上下文切换有很多相似的地方。
系统调用的开销
strace
命令
strace命令
来查看到你的程序正在执行哪些系统调用
# strace -p 28927
Process 28927 attached
epoll_wait(6, {{EPOLLIN, {u32=96829456, u64=140312383422480}}}, 512, -1) = 1
accept4(8, {sa_family=AF_INET, sin_port=htons(55465), sin_addr=inet_addr("10.143.52.149")}, [16], SOCK_NONBLOCK) = 13 epoll_ctl(6, EPOLL_CTL_ADD, 13, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=96841984, u64=140312383435008}}) = 0
epoll_wait(6, {{EPOLLIN, {u32=96841984, u64=140312383435008}}}, 512, 60000) = 1
开销
相比较函数调用时的不到1ns的耗时,系统调用确实开销蛮大的。虽然使用了“快速系统调用”指令,但耗时仍大约在200ns+,多的可能到十几us。每个系统调用内核要进行许多工作,大约需要执行1000条左右的CPU指令,所以确实应该尽量减少系统调用。但是即使是10us,仍然是1ms的百分之一,所以还没到了谈系统调用色变的程度,能理性认识到它的开销既可。
另外为什么系统调用之间的耗时相差这么多?因为系统调用花在内核态用户态的切换上的时间是差不多的,但区别在于不同的系统调用当进入到内核态之后要处理的工作不同,呆在内核态里的时候相差较大。
函数调用的开销
C语言
每个c函数调用耗时大约是0.4ns左右。
每个c函数需要的CPU指令数是8个!
8次CPU指令中大部分都是寄存器的操作,即使有“内存IO”,也是在栈上进行。而栈操作密集,符合局部性原理,早就被L1缓存住了,其实都是L1的IO,所以耗时很低
其他语言
- php7:1000W次耗时0.667s,减去0.140s的for循环耗时,平均每次函数调用耗时52ns
- php53:1000W次耗时2.1s,减去0.5s的for循环耗时,平均每次耗时160ns
php的函数调用确实比c的要慢很多,从不到1ns升高到了50ns左右。因为php又用c虚拟了一层指令集,这层指令集还需要变成CPU的指令集后才可以真正运行。但是要知道的是ns这个时间单位太小了,假如你用的框架特别变态,一个用户请求来了直接就搞了1000次的函数调用,那么消耗在函数调用上的时间会是50ns*1000=50us。这和代码框架化后给团队项目带来的便利性来对比的话,这点时间开销,我觉得仍然是可以忽略的。