系统性能优化-2 CPU

系统性能优化-2 CPU

其实除了 CPU 的频率,多核架构以及多 CPU 架构对系统运行的性能也是很大影响的,那么该如何充分利用 CPU 呢?

CPU 架构

首先介绍一下当前主流的 CPU 架构,现在的系统基本都是多 CPU,一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1 cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。同时,一个 CPU 上的所有物理核共享一个三级缓存。

Centos 可以使用 lscpu 查看系统 CPU 信息

bash 复制代码
[root@VM-16-11-centos zwj]# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2
On-line CPU(s) list:   0,1
Thread(s) per core:    1
Core(s) per socket:    2
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 94
Model name:            Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:              3
CPU MHz:               2394.364
BogoMIPS:              4788.72
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
L3 cache:              28160K
NUMA node0 CPU(s):     0,1

程序在执行时,会先将内存中的数据载入三级缓存,再进入每颗核心独有的二级缓存,最后进入最快的一级缓存,之后才会被 CPU 使用。

缓存比内存的访问速度要快很多,访问内存要100个时钟周期以上,一级缓存只需要4~5个时钟周期,二级缓存大约12个时钟周期,三级缓存大约30个时钟周期。因此如果 CPU 所要操作的数据可以命中缓存,会带来一定的性能提升。

NUMA 架构

一个 CPU 的结构我们清楚了,但现在的系统基本都是多 CPU(也称为多 CPU Socket),CPU 之间通过总线连接,如下图所示:

在多 CPU 架构上,应用程序可以在不同的 CPU 上执行,如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。

在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,我们也把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)

所以 CPU 架构对应用程序的运行主要有以下影响:

  • cpu 缓存访问速度远远大于内存,因此尽量缓存命中,多利用缓存
  • NUMA 架构可能会出现远端内存访问的情况,这会直接增加应用程序的执行时间

提升缓存命中率

Centos 运行 lscpu

bash 复制代码
[root@VM-16-11-centos zwj]# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
L3 cache:              28160K
NUMA node0 CPU(s):     0,1

可以看到 L1 cache 并不是单独的,而是分为 L1d cache 和 L1i cache,也就是 数据缓存 和 指令缓存,因此提高缓存命中率也需要分别考虑

数据缓存

思考这样一个场景,有一个二维数组需要进行一次遍历

c 复制代码
int arr[10][10];

for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 10; i++) {
        printf("%d", arr[i][j])
    }
}
c 复制代码
int arr[10][10];

for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 10; i++) {
        printf("%d", arr[j][i])
    }
}

不要思考这个场景是否合理,比如需要遍历一个数组把所有值加入到一个 set 中,思考这两种方式的执行速度,其实方式1是远优于方式2的(如果是 Python,会由于数组的设计效果没那么明显)。

原因是 CPU 其实有 Cache Line 的概念,CPU 在读取数据时会一次性读取 Cache Line 大小的数据到缓存中,而我们又知道数组在内存中是紧密排放的

c 复制代码
arr[0][0] arr[0][1] arr[0][2]
arr[1][0]    
arr[2][0]    

如果按照第一种方式遍历,就可以利用 CPU 缓存加快访问

Linux 可以通过 cat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size 查看 Cache Line 大小

bash 复制代码
[root@VM-16-11-centos zwj]# cat /sys/devices/system/cpu/cpu0/cache/index1/coherency_line_size 
64

Cache Line 在 nginx、redis 的设计中都有所体现:

  • nginx 使用哈希表存放域名、http头部信息,哈希表里桶的大小如 server_names_hash_bucket_size,它默认就等于 CPU Cache Line 的值。由于所存放的字符串长度不能大于桶的大小,所以当需要存放更长的字符串时,就需要修改桶大小,但 Nginx 官网上明确建议它应该是 CPU Cache Line 的整数倍。
  • redis 的 sds 中,embstr 的 44 字节,其实也是 CPU Cache Line 的体现,当 robj + 字符串 + '\0' 的长度不超过 64 时,此时编码就是 embstr,在读取到 robj 时就直接取到了字符串数据,无需再次访问内存。

执行 perf stat 可以统计出进程运行时的系统信息(通过 -e 选项指定要统计的事件,如果要查看三级缓存总的命中率,可以指定缓存未命中 cache-misses 事件,以及读取缓存次数 cache-references 事件,两者相除就是缓存的未命中率,用 1 相减就是命中率。类似的,通过 L1-dcache-load-misses 和 L1-dcache-loads 可以得到 L1 缓存的命中率)

指令缓存

上面只是介绍了提高数据缓存的命中率,还有指令缓存的命中率

假如有一个数组,里面是一些 0~255 的数字,需要找出其中 < 128 的置为 0 并进行排序,方法1是先找到对应数据并置为0再排序,方法2是先排序再置为0。

bash 复制代码
for(i = 0; i < N; i++) {
       if (array [i] < 128) array[i] = 0;
}
sort(array, array + N);
bash 复制代码
sort(array, array + N);
for(i = 0; i < N; i++) {
       if (array [i] < 128) array[i] = 0;
}

先排序的方法速度其实是要更快的,原因是因为循环中有大量的 if 条件分支,而 CPU含有分支预测器 。当代码中出现 if、switch 等语句时,意味着此时至少可以选择跳转到两段不同的指令去执行。如果分支预测器可以预测接下来要在哪段代码执行(比如 if 还是 else 中的指令),就可以提前把这些指令放在缓存中,CPU 执行时就会很快。当数组中的元素完全随机时,分支预测器无法有效工作,而当 array 数组有序时,分支预测器会动态地根据历史命中数据对未来进行预测,命中率就会非常高

绑核

多 CPU 和多核心是不可避免的,为了缓解 NUMA 及应用程序重新调度后需要再次加载数据到 CPU 缓存的问题,操作系统提供了绑核指令,ls cpu 指令可以查看每个 CPU 对应的核心编号,可以使用 taskset 命令行指令在启动时将应用程序与 CPU 进行绑定,也可以使用不同语言的 API (如sched_setaffinity)通过系统调用在程序代码中设置进程的 CPU 亲和性。此外,Perf 工具也提供了 cpu-migrations 事件,它可以显示进程从不同的 CPU 核心上迁移的次数。

绑核是把双刃剑,如果你的程序有很多的后台线程,例如 redis 需要子线程生成 rdb、aof 重写、删除过期 key,就会导致子进程、后台线程和主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。当然也有缓解的办法,比如让程序绑定一个具有多个逻辑核心的核,可以在一定程度上缓解 CPU 资源竞争。

不过 Redis 6 好像已经提供了支持 CPU 核绑定的配置操作了~

相关推荐
Mrdaliang6 小时前
Linux系统性能优化
linux·运维·性能优化
前端小菜嘤6 小时前
性能优化相关
性能优化
北苇渡江7 小时前
Nginx代理缓存静态资源
nginx
朱小勇本勇10 小时前
Clang Code Model: Error: The clangbackend executable “D:\Soft\Qt5.12.12\Tool
运维·服务器·数据库·qt·nginx
码上库利南11 小时前
详细讲解Redis为什么被设计成单线程
数据库·redis·缓存
W说编程11 小时前
算法导论第十四章 B树与B+树:海量数据的守护者
c语言·数据结构·b树·算法·性能优化
玺同学12 小时前
从卡顿到流畅:前端渲染性能深度解析与实战指南
前端·javascript·性能优化
1892280486115 小时前
NY313NY314美光固态闪存NY315NY316
服务器·科技·性能优化
云_杰16 小时前
HarmonyOS性能优化——资源提前加载
性能优化·harmonyos