性能优化(应用程序、cpu)

自己在极客时间课程、工作经验、技术博客的总结。

基本知识

时间量级

延时: 发生一次操作的时间均值

相对时间: 已一个cpu周期为1s的参照物,进行等比放大

事件 延时 相对时间比例
1个CPU周期 0.3ns 1s
L1缓存访问 0.9ns 3s
L2缓存访问 2.8ns 9s
互斥加锁 10ns 30s
L3缓存访问 12.9ns 43s
主存访问 120ns 6分钟
上下文切换 1微秒 1小时
固态硬盘I/O 50~160微秒 2~6天
旋转磁盘I/O 1~10ms 1~12月
互联网:从北京到深圳(2000km) 20ms 2年
TCP包重传 1~2s 105~317年
OS虚拟化系统重启 4s 423年

指标

  • IOPS: 每秒I/O 操作数
  • 吞吐量: 每秒数据量或操作量
  • 使用率
  • 延时
  • 饱和度

缓存

命中率越高越好。

算法:

  • 最近最常使用算法(MRU)
  • 最近最少使用算法(LRU)
  • 最常使用算法(MFU)
  • 最不常使用算法(LFU)
  • 不常使用算法(NFU)

缓存的状态:

  • 冷: 冷缓存时空的,或者填充无用信息。 命中率几乎为0
  • 热:填充常用数据,有着很高的命中率
  • 温:填充了部分有效数据,但命中率还没达到预期
  • 热度:缓存命中率

应用程序

技术

  • 选择合适的I/O尺寸
  • 用好缓存
  • 缓冲区
  • 轮询。 epoll替换poll
  • 并发和并行。使用自适应锁、读写锁
  • 非阻塞I/O
  • 处理器绑定。提高应用程序的内存本地性,减少内存I/O
  • 编译器优化
  • 垃圾回收(GC)
  • 线程池
  • 零拷贝
  • 协程
  • 数据结构、算法选择
  • 多线程替换多进程

CPU

基本知识

  • 处理器(socket):物理芯片
  • 多核:一个独立cpu实例。一个处理器可以包含多个cpu实例
  • 硬件线程(逻辑核):一个核上同时执行多个线程(包括intel的超线程HT)。

举例 如果一台计算机有两个处理器,每个处理器有 12 个核,而且采用了 HT 超线程(一般一个核两个HT),那么总的 CPU 数目就是 48,就是 2×12×2。这个数字 48,就是用监控软件实际看到的cpu数量。对于操作系统来说就是cpu0、cpu1、cpu2…cpu47.

概念

字长

处理器是围绕最大字长设计的–32位或者64位。更宽的字长意味着更好的性能。更宽的字长可能会在某些数据类型下因未使用的位而导致额外的内存开销,数据的大小也会因为指针大小的增加而增加,导致需要更多的内存I/O。但对于64位的x86架构来说,寄存器的增加核更有效的调用约定抵消了这些开销,因此64位的应用程序会比他们32位的版本跑得更快。如果软件被编译成较小的字长,它可能会慢很多。

多处理器和NUMA架构

现在的 CPU 普遍采用多处理器(Socket)来提高 CPU 性能,每个处理器都有自己可以直接访问的本地内存(Local Memory)。一般来讲,这里面每个处理器的性能和内存大小都是一样的。每个处理器也都可以访问其他处理器的内存,这些内存就相当于是外地 / 远程内存(Remote Memory)。当 CPU 处理器访问本地内存时,会有较短的响应时间(称为本地访问 Local Access)。而如果需要访问外地/远程内存时候,就需要通过互联通道访问,响应时间就相比本地内存变慢了(称为远端访问 Remote Access)。所以 NUMA(Non-Uniform Memory Access)就此得名。
采用多处理器和 NUMA 架构的主要原因,是提高整个 CPU 的并行处理性能。每一台服务器可以同时运行很多程序和进程。对每一个进程和线程而言,当它运行在某一个处理器上时,它所对应的内存使用默认的分配方案是——优先尝试在请求线程当前所处的处理器的本地内存上分配。如果本地内存不足,才会分配到外地 / 远程内存上去。

编译器优化

通过编译器选项(包括字长设置)来大幅改进;编译器更新以利用最新的cpu指令集进行优化。

CPU内存缓存

  • 一级指令缓存
  • 一级数据缓存
  • 转译后备缓存器(TLB)
  • 二级缓存
  • 三级缓存

其中L1、L2是核独享的,L3是处理器所有核共享的

缓存行(cache line)\缓存一致性

内存缓存系统中,已缓存行作为单位存储。x86处理典型的缓存大小是64字节。现代 CPU 为了保证缓存相对于内存的一致性,必须实时监测每个核对缓存相对应的内存位置的修改。内存可能会同时被缓存在不同处理器的多个cpu里,当一个cpu修改了内存,所有的缓存需要知道他们的缓存copy已经失效,应该被丢弃,这将导致核与核之间产生竞争关系,因为一个核对内存的修改,将导致另外的核在该处内存上的缓存失效,那么就需要从L3或从主存中重新获取数据,这将带来更大的I/O。在多线程的场景下就会导致这样的问题。当多线程修改看似互相独立的变量时,如果这些变量共享同一个缓存行,就会在无意中影响彼此的性能,这就是伪共享。

自愿上下文切换(VCX/cswch)

是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。

非自愿上下文切换(ICX/nvcswch)

是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

性能指标

  • cpu负载情况
  • cpu利用率
  • 应用程序、线程的cpu利用率
  • cpu中断性能:软中断、硬中断、上下文切换

CPU性能常见问题表现

  • 中断:各个核中断不均衡,导致cpu超载或空闲
  • cpu超载:多核之间负载没有平衡;cpu做了无用功;应用程序设计需要优化;…
  • cpu空闲:太多内存操作,导致cpu停顿;太多的分支预测错误;…

分析工具

  • top
  • uptime
  • mpstat/pidstat
  • vmstat
  • perf

调优

提高最后一级缓存(LLC)命中率


perf stat -e LLC-loads,LLC-load-misses,LLC-stores,LLC-store-misses
  • 缩小数据结构,让数据变的紧凑(如c++ folly库F14ValueMap)
  • 用软件方式来预取数据

现代 CPU 其实一般都有硬件指令和数据预取功能,也就是根据程序的运行状态进行预测,并提前把指令和数据预取到缓存中。这种硬件预测针对连续性的内存访问特别有效。但是在相当多的情况下,程序对内存的访问模式是随机、不规则的,也就是不连续的。硬件预取器对于这种随机的访问模式,根本无法做出正确的预测,这就需要使用软件预取。软件预取就是这样一种预取到缓存中的技术,以便及时提供给 CPU,减少 CPU 停顿,从而降低缓存的不命中率,也就提高了 CPU 的使用效率。现代 CPU 都提供相应的预取指令,具体来讲,Windows 下可以使用 VC++ 提供的 _mm_prefetch 函数,Linux 下可以使用 GCC 提供的 __builtin_prefetch 函数。GCC 提供了这样的接口,允许开发人员向编译器提供提示,从而帮助 GCC 为底层的编译处理器产生预取指令。这种策略在硬件预取不能正确、及时地预取数据时,极为有用。但是软件预取也是有代价的。一是预取的操作本身也是一种 CPU 指令,执行它就会占用 CPU 的周期。更重要的是,预取的内存数据总是会占用缓存空间。因为缓存空间很有限,这样可能会踢出其他的缓存的内容,从而造成被踢出内容的缓存不命中。如果预取的数据没有及时被用到,或者带来的好处不大,甚至小于带来的踢出其他缓存相对应的代价,那么软件预取就不会提升性能。我自己在这方面的实践经验,有这么几条:软件预取最好只针对绝对必要的情况,就是对会实际严重导致 CPU 停顿的数据进行预取。对于很长的循环(就是循环次数比较多),尽量提前预取后面的两到三个循环所需要的数据。而对于短些的循环(循环次数比较少),可以试试在进入循环之前,就把数据提前预取到。

  • 去除伪共享缓存(缓存一致性问题)

这个问题的解决方案,是让每个元素单独占用一个缓存行,比如 64 字节,也就是按缓存行的大小来对齐(Cache Line Alignment)。具体方法怎么实现呢?其实就是插入一些无用的字节(Padding)。这样的好处,是多个线程可以修改各自的元素和对应的缓存行,不会存在缓存行竞争,也就避免了“伪共享”问题。

  • 进程和线程进行CPU绑定。避免切换cpu时L1、L2 缓存失效
  • cache line : 使用cache line每次至少连续读取64字节的特性,减少缓存访问。

cpu绑定、cpu独占

把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些 CPU。

资源控制

使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。

优先级调整

使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。在这里,适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理。

中断的负载均衡

无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的 CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把中断处理过程自动负载均衡到多个 CPU 上。

编译器选项

一般编译为64位而非32位进程;以及优化级别设置

方法论

“千万避免过早优化”。

过高德纳的这句名言, “过早优化是万恶之源”,过早优化不可取。因为,一方面,优化会带来复杂性的提升,降低可维护性;另一方面,需求不是一成不变的。针对当前情况进行的优化,很可能并不适应快速变化的新需求。这样,在新需求出现时,这些复杂的优化,反而可能阻碍新功能的开发。所以,性能优化最好是逐步完善,动态进行,不追求一步到位,而要首先保证能满足当前的性能要求。当发现性能不满足要求或者出现性能瓶颈时,再根据性能评估的结果,选择最重要的性能问题进行优化。

你可能感兴趣的:(工作积累,性能优化,服务器,linux,windows)