超线程 – 线程级并行

Pentium 4

  1. Pentium 4失败的原因:CPU的流水线级数太深
  2. 超长的流水线,使得之前很多解决冒险提升并发的方案都用不上
  3. 解决冒险、提升并发的方案,本质上是一种指令级并行的技术方案,即CPU希望在同一个时间,去并行执行两条指令
  • 但这两条指令,原本在代码里是有先后顺序
  1. 无论是流水线架构分支预测以及乱序执行,还是超标量超长指令字
  • 都是想通过在同一时间执行两条指令,来提升CPU的吞吐率
  • 但在Pentium 4上,上面这些方法都可能因为流水线太深,而起不到效果
  • 更深的流水线意味着同时在流水线里面的指令就很多,相互的依赖关系就多
  • 因此,很多时候不得不把流水线停顿下来,插入很多NOP操作,来解决这些依赖带来的冒险问题

超线程

  1. 无论是多个CPU核心运行不同的程序,还是单个CPU核心里切换运行不同线程的任务
    • 同一时间点上,一个物理的CPU核心只会运行一个线程的指令,其实并没有做到真正的指令级并行
  2. 超线程的CPU,把一个物理层面的CPU核心,伪装成两个逻辑层面的CPU核心
    • 这个CPU会在硬件层面增加很多电路,使得可以在一个CPU核心内部,维护两个不同线程的指令的状态信息
    • 在一个物理CPU核心内部,会有双份PC寄存器指令寄存器条件码寄存器
    • 在外面看来,似乎有两个逻辑层面的CPU在同时运行
    • 因此,超线程技术也被叫为同时多线程(Simultaneous Multi-Threading,SMT)技术
  3. 但CPU的其它功能组件,没有提供双份,无论是指令译码器还是ALU,一个物理CPU核心仍然只有一份
    • 因为超线程并不是真的去同时运行两个指令
    • 超线程的目的:在线程A的指令在流水线停顿的时候,让线程B去执行指令,此时CPU的指令译码器ALU空闲
      • 线程B没有对线程A里面的指令有关联和依赖
  4. CPU通过很小的代价,就能实现同时运行多个线程的效果
    • 只需要在CPU核心增加10%左右的逻辑功能,增加可以忽略不计的晶体管数量
  5. 超线程并没有增加功能单元(ALU),所以超线程只在特定的应用场景下效果比较好
    • 一般是各个线程等待时间比较长的应用场景
    • 例如需要应对很多请求的数据库应用,就比较适合使用超线程,各个指令都要等待访问内存数据,但并不需要做太多计算

SIMD – 指令级并行

  1. SIMD:Single Instruction Multiple Data,单指令多数据流,支持SIMD的指令集:MMXSSE
  2. 两段代码
    • 通过循环的方式,给list里面的每一个数加1
    • 实现相同的功能,直接调用NumPy库的add方法
    • 性能差异:32.72
  3. 原因:NumPy直接用了SIMD指令,能够并行进行向量的操作
    • 通过循环来一步一步计算的算法,称为SISD单指令单数据
    • 如果是多核CPU,可以同时处理多个指令的方式称为MIMD多指令多数据
  4. SIMD在获取数据执行指令的时候,都做了并行
    • 从内存读取数据的时候,SIMD一次性读取多个数据
      • 下面程序数组里面的元素是integer,需要4Bytes的内存空间
      • Intel在引入SSE指令集的时候,在CPU里添加了8个128Bits的寄存器
        • 128Bits ≈ 16Bytes,即一个寄存器可以一次性加载4个整数
        • 比循环分别读取4次对应的数据,能节省不少时间
    • 在数据读取之后,到了指令的执行层面,SIMD也是可以并行执行
      • 4个整数各自加1,互相之间完全没有依赖,即不需要处理冒险问题
      • 只要CPU里有足够的功能单元,能够同时进行这些计算,那这个加法就是4路同时并行
      • 因此那些在计算层面存在大量『数据并行』的计算中,使用SIMD能够很好地提升性能
    • 实践:向量运算(同一向量的不同维度之间的计算是相互独立的)、矩阵运算
      • 图片、视频、音频的处理
      • 机器学习算法的计算
  5. 基于SIMD向量计算指令,是在Intel发布Pentium处理器的时候引入的指令集
    • 当时的指令集叫作MMX(Matrix Math eXtensions,矩阵数学扩展
    • Pentium处理器,第一个有能力进行多媒体处理的CPU
1
2
3
4
5
6
7
8
9
$ python
>>> import numpy as np
>>> import timeit
>>> a = list(range(1000))
>>> b = np.array(range(1000))
>>> timeit.timeit("[i + 1 for i in a]", setup="from __main__ import a", number=1000000)
32.260748863220215
>>> timeit.timeit("np.add(1, b)", setup="from __main__ import np, b", number=1000000)
0.9859158992767334

参考资料

深入浅出计算机组成原理