系统性能调优必知必会

2021-09-08  本文已影响0人  技术灭霸

01 | CPU缓存:怎样写代码能够让CPU执行得更快?

CPU缓存通常分为大小不等的三级缓存


我们的代码优化目标是提升CPU缓存的命中率。

遇到遍历访问数组的情况时,按照内存布局顺序访问将会带来很大的性能提升。

  1. 当array数组有序,CPU含有分支预测器会动态地根据历史命中数据对未来进行预测,命中率就会非常高
  2. 由于缓存是根据CPU Cache Line批量操作数据,顺序操作内存也有性能提升。

如果将每个线程分别绑定在不同的CPU核心上,性能便会获取很大的提升,Linux上提供sched_setaffinity。Perf工具也提供了cpu-migrations事件,可以显示进程从不同的CPU核心上迁移的次数。

02 | 内存池:如何提升内存分配的效率?

在业务代码和操作系统内核,有两层内存池,Java已经有应用内存池,为什么还要c库内存池呢?
因为除了jvm管理的堆内存外,java还有堆外内存,由于它不使用jvm的垃圾回收机制,所以更稳定、持久、处理IO的速度也更快。

Linux下的JVM编译时默认使用Ptmalloc2内存池,每个线程都预分配了64MB,可以提升后续内存分配的性能。

不过也可以通过MALLOC_ARENA_MAX环境变量,可以限制线程内存池的最大数量。

TCMalloc对多线程下的小内存分配特别友好,而Ptmalloc2则对各类尺寸的内存申请都有稳定的表现,更加通用。

从堆还是栈上分配内存?

为什么栈中分配内存更快呢?

每个线程都有自己的栈,分配内存时不需要加锁保护,而且栈上对象的尺寸在编译阶段就已经写入可执行文件了,执行效率更高。

缺点:

  1. 栈内存生命周期有限。随着函数调用结束后自动释放。
  2. 栈的容量有限。

当我们分配内存时,如果在满足功能的情况下,可以在栈中分配的话,就选择栈。

03 | 索引:如何用哈希表管理亿级对象?

哈希表基于数组实现,而数组可以根据下标随机访问任意元素。数组之所以随机访问,是因为它由连续内存承载,且每个数组元素的大小都相等,于是,当我们知道下标后,把下标乘以元素大小,再加上数组的首地址,就可以获取目标访问地址,直接获取数据

所以,哈希函数的执行时间是常量,数组的随机访问也是常量,时间复杂度就是O(1)

有两种方法解决哈希冲突:

  1. 链接法
  2. 开放寻址法(更擅长序列化数据,确保所有对象都在数组里,就可以把数组用到的这段连续内存原地映射到文件中)

降低哈希表的冲突概率

  1. 调优哈希函数
  2. 扩容

转载因子越接近于1,冲突概率越大,我们不能改变元素的数量,只能通过扩容提升哈希桶的数量,减少冲突。

使用哈希表,要注意几个关键问题

  1. 生产环境一定要考虑容灾,把哈希表原地序列化为文件是一个解决方案,它能保证新进程快速恢复哈希表。
  2. 注重内存的节约使用
  3. 优化哈希函数

04 | 零拷贝:如何高效地传输文件?

磁盘是主机中最慢的硬件之一,常常是性能瓶颈,所以优化它能够获取立竿见影的效果

所以针对磁盘的优化技术层出不穷,比如零拷贝、直接IO、异步IO等等,这些技术降低操作时延,提升系统的吞吐量,围绕内核的磁盘高速缓存(PageCache)去减少CPU和磁盘设备的工作量

image.png

要想提升传输文件的性能,从降低上下文切换的频率和内存拷贝次数两个方向入手。

2次与用户缓存区相关的拷贝动作都不是必需的,因为在磁盘文件发到网络的场景中,用户缓存区没有必须存在的理由。



这就是零拷贝技术

异步IO + 直接IO

高并发场景处理大文件时,应当使用异步IO和直接IO来替换零拷贝。


绕过PageCache的IO,叫直接IO,对于磁盘,异步IO只支持直接IO。

直接IO的应用场景

  1. 应用程序已经实现了磁盘文件的缓存,不需要PageCache再次缓存
  2. 高并发传输大文件,大文件难以命中PageCache缓存,同时还挤掉小文件使用PageCache需要的内存

05 | 协程:如何快速地实现高并发服务?

如何快速地实现高并发服务?

要想实现高并发,一个简单的做法就是多线程,为每个请求分配一个线程来执行。但多线程的方式也是有弊端的,如下:

  1. 单个线程消耗内存过多,没有足够的内存去创建几万线程实现并发
  2. 切换请求是内核通过切换线程来实现的,线程的切换就会带来上下文的切换,也是会耗费 CPU 资源的

如何破?

把本来由内核实现的请求切换工作交给用户态的代码来完成,这样可以降低切换成本和内存占用

异步编程可以实现用户态的请求切换。异步化依赖 IO 多路复用机制的同时,还需要把阻塞方法改为非阻塞方法

比如一个线程处理两个请求,请求 1 过来通过异步框架发起异步 IO 读,同时向异步框架注册回调函数。然后切换到请求 2,由异步框架发起异步 IO 读,同样也会注册回调函数。
最后由异步框架依赖 IO 多路复用机制来检查数据是否就绪,如果数据就绪就通过之前请求注册的回调函数去处理

异步代码不好写,容易出错,我们项目中用的 vertx 异步框架,我到现在也写不好异步代码。

协程可以弥补异步框架的不足,其实协程是建立在异步的基础上的,他俩都是使用非阻塞的系统调用与内核交互,把请求切换放到用户态。他俩不同的地方在于,协程把异步化中的两段函数封装成一个阻塞的协程函数。在该函数执行时,由协程框架完成协程之间的切换,协程是无感知的

协程是如何完成切换的?

在用户态完成协程的切换和在内核态完成线程的切换原理类似。
每个协程有独立的栈,一般占用空间选小于线程的栈,(协程一般是几十 KB,线程是 8MB)所以相同的内存空间可以创建更多的协程来处理请求。栈中保存了函数的调用关系、参数和返回值。CPU 中的栈寄存器 SP 指向当前协程的栈,指令寄存器 IP 保存下一条执行的指令的地址。

在协程 1 切换到协程 2 时要把协程 1 的 SP 和 IP 寄存器的值保存下来,再从内存中找到协程 2 上一次切换前保存的寄存器值,写入到 CPU 的寄存器,这样就完成了协程的切换


协程是用户态的线程,一个线程可以包含多个协程,要保证协程的切换由用户态代码完成,如果协程触发了线程的切换就会导致该线程上的所有协程都阻塞,因为线程的切换是由内核态完成的

所以,协程的高性能,建立在切换必须由用户态代码完成之上,这需要协程的生态是完整的,尽量覆盖常见的组件,go 好像是天然支持协程,Java 的协程生态现在应该还不成熟,用的比较少。

上一篇下一篇

猜你喜欢

热点阅读