Skip to content

Latest commit

 

History

History
143 lines (106 loc) · 13.2 KB

cpu.md

File metadata and controls

143 lines (106 loc) · 13.2 KB

CPU

CPU 组成

CPU组成

CPU 流水线

CPU流水线

  • 同步时钟周期,不再是指令级别,而是 流水线阶段级别。每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理。每增加一级的流水线,就要多一级写入到流水线寄存器的操作。
  • 流水线技术并不能缩短单条指令的响应时间这个性能指标,但是可以增加在运行很多条指令时候的 吞吐率。因为不同的指令,实际执行需要的时间是不同的。
  • 流水线带来的吞吐率提升,只是一个理想情况下的理论值。在实践的应用过程中,还需要解决指令之间的依赖问题。要想解决好冒险的依赖关系问题,需要引入乱序执行、分支预测等技术。
  • 如果发现了后面执行的指令,会对前面执行的指令有数据层面的依赖关系,最简单的办法就是"再等等"。在进行指令译码的时候,会拿到对应指令所需要访问的寄存器和内存地址。所以这个时候能够判断出来,这个指令是否会触发数据冒险。如果会触发数据冒险,就可以决定,让整个流水线停顿一个或者多个周期。
  • 在流水线里,后面的指令不依赖前面的指令,那就不用等待前面的指令执行,它完全可以先执行。

分支预测

分支预测 预测错误

  • 最简单的分支预测技术,叫作"假装分支不发生"。就是仍然按照顺序,把指令往下执行。其实就是 CPU 预测,条件跳转一定不发生。这样的预测方法,也是一种静态预测技术。
  • 如果分支预测是正确的,节省下来本来需要停顿下来等待的时间。如果分支预测失败了,那就把后面已经取出指令已经执行的部分,给丢弃掉。这个丢弃的操作,在流水线里面,叫作 Zap 或者 Flush。CPU 不仅要执行后面的指令,对于这些已经在流水线里面执行到一半的指令,还需要做对应的清除操作。清空已经使用的寄存器里面的数据等等,这些清除操作,也有一定的开销。
  • 现代流水线 CPU 通常会采用一种名为"投机执行"的方式来优化条件跳转指令的执行。所谓投机执行,是指 CPU 会通过分析历史的分支执行情况,来推测条件跳转指令将会执行的分支,并提前处理所预测分支上的指令。而等到 CPU 发现之前所预测的分支是错误的时候,将不得不丢弃这个分支上指令的所有中间处理结果,并将执行流程转移到正确的分支上。很明显,这样就会浪费较多的时钟周期。

超线程

超线程核心

  • 超线程的 CPU,把一个物理层面 CPU 核心,伪装成两个逻辑层面的 CPU 核心。会在硬件层面增加很多电路,使可以在一个 CPU 核心内部,维护两个不同线程的指令的状态信息。
  • 在 CPU 的其他功能组件上,Intel 不会提供双份。无论是指令译码器还是 ALU,一个 CPU 核心仍然只有一份。因为超线程并不是真的去同时运行两个指令,那就真的变成物理多核了。
  • 超线程的目的,是在一个线程 A 的指令,在流水线里停顿的时候,让另外一个线程去执行指令。因为这个时候,CPU 的译码器和 ALU 就空出来了,那么另外一个线程 B,就可以拿来干自己需要的事情。这个线程 B 可没有对于线程 A 里面指令的关联和依赖。

指令集

CPU指令集

  • CPU 的指令集里的机器码是固定长度还是可变长度,也就是复杂指令集(Complex Instruction Set Computing,简称 CISC)和精简指令集(Reduced Instruction Set Computing,简称 RISC)。

高速缓存

CPU高速缓存 CPU缓存行

  • 为了弥补 CPU 与内存两者之间的性能差异,能真实地把 CPU 的性能提升用起来,而不是让它在那儿空转,在现代 CPU 中引入了 高速缓存
  • 每个 CPU 核心都有一块属于自己的 L1 高速缓存,通常分成指令缓存和数据缓存,分开存放 CPU 使用的指令和数据。
  • L2 的 Cache 同样是每个 CPU 核心都有的,不过往往不在 CPU 核心的内部。所以 L2 的访问速度会比 L1 稍微慢一些。
  • L3 通常是多个 CPU 核心共用的,尺寸会更大一些,访问速度自然也就更慢一些。
  • 从 CPU Cache 被加入到现有的 CPU 里开始,内存中的指令、数据,会被加载到 L1 ~ L3 Cache 中,而不是直接由 CPU 访问内存去拿。在 95% 的情况下,CPU 都只需要访问 L1 ~ L3 Cache,从里面读取指令和数据,而无需访问内存。
  • CPU 从内存中读取数据到 CPU Cache 的过程中,是一小块一小块来读取数据的,而不是按照单个数组元素来读取数据的。这样一小块一小块的数据,在 CPU Cache 里面,叫作 Cache Line
  • 在 CPU Cache 里,对于数据的写入,有 写直达写回 这两种解决方案。写直达把所有的数据都直接写入到主内存里面,简单直观,但是性能就会受限于内存的访问速度。而写回则通常只更新缓存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到主内存里。在缓存经常会命中的情况下,性能更好。

局部性原理

  • 时间局部性。如果一个数据被访问了,那么它在 时间内还会被再次访问。
  • 空间局部性。如果一个数据被访问了,那么和它 相邻 的数据也很快会被访问。
  • 有了时间局部性和空间局部性,不用再把所有数据都放在内存里,也不用都放在硬盘上,而是把访问次数多的数据,放在贵但是快一点的存储器里,把访问次数少的数据,放在慢但是大一点的存储器里。

用户态线程模型

  • 一对一模型,一个用户级别线程对应一个内核级别线程,可能会影响系统的性能。即使一个用户线程发生阻塞,也不会影响该进程的执行。
  • 多对一模型,创建多个用户级线程,但实际上只有一个内核级线程在运行,并没有实现并行。
  • 多对多(两级线程)模型,允许多个用户级线程复用到更小或者数量相同内核级线程上,可以创建任意多的用户级线程,而相应的内核级线程可以在多核处理器上正常运行。当一个线程被阻塞,内核也会调度其他的线程来处理。

中断

  • 中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。
  • Linux 中的中断处理程序分为上半部和下半部:
    • 上半部对应 硬件中断,用来快速处理中断。
    • 下半部对应 软中断,用来异步处理上半部未完成的工作。每个 CPU 都对应一个软中断内核线程,名字为 "ksoftirqd/CPU 编号"。
  • Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的运行情况。

上下文切换

  • 系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。
  • 进程的切换只能发生在内核态。进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
  • Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
  • 中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
  • 自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说,I/O、内存等系统资源不足时,就会发生自愿上下文切换。
  • 非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

性能分析

CPU性能分析

  • 用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。
  • 系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。
  • 等待 I/O 的 CPU 使用率,通常也称为 iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
  • 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
  • 在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。
  • 平均负载是指单位时间内,处于可运行状态和不可中断状态的进程数。所以,它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程。CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。平均负载高并不一定代表 CPU 使用率高,还有可能是 I/O 更繁忙了。
  • CPU 绑定:使用 sched_setaffinity() 函数 或 taskset -cp core_number pid 命令把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
  • CPU 独占:跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些 CPU。
  • 优先级调整:使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理。
  • 为进程设置资源限制:使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。
  • NUMA(Non-Uniform Memory Access)优化:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。NUMA 优化,其实就是让 CPU 尽可能只访问本地内存。
  • 中断负载均衡:无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的 CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把中断处理过程负载均衡到多个 CPU 上。

分析工具

传统工具

BPF 工具

  • execsnoop[-bpfcc/.bt]

  • exitsnoop[-bpfcc]

  • profile[-bpfcc]

    # 火焰图
    profile[-bpfcc] -af 30 > out.stacks
    git clone https://github.com/brendangregg/FlameGraph
    cd FlameGraph
    ./flamegraph.pl --color=java < ../out.stacks > out.svg
    
    #bpftrace单行程序
    bpftrace -e 'profile:hz:49 /pid/ { @samples[ustack, kstack, comm] = count(); }'
  • offcputime[-bpfcc]