cpu-硬件性能

时钟频率(clock rate)

是指同步电路中时钟的基础频率,即每秒的时钟周期数,CPU的时钟频率通常是由晶体振荡器的频率决定的

比如 Intel Core-i7-7700HQ 2.8GHz,它的CPU时钟频率为 2.8GHz,可以理解为每秒震荡 2.8G 次

速度是矢量,频率为标量,都是用来描述快慢


时钟周期(clock cycle)

时钟频率的倒数,是计算机中最基本的、最小的时间单位,是一个时间的量

比如时钟频率为 2.8GHz,1个时钟周期需要 1/2.8G 秒


单指令周期处理器(Single Cycle Processor)

在一个时钟周期内,处理器正好能处理一条指令采用这种设计思路的处理器,就叫作单指令周期处理器

不同指令的执行时间不同,但是我们需要让所有指令都在一个时钟周期内完成,那就只好把时钟周期执行时间最长的那个指令设成一样,这样会导致那些执行速度快的指令浪费了很多等待时间


所以,在单指令周期处理器里面,无论是执行一条用不到 ALU 的无条件跳转指令,还是一条计算起来电路特别复杂的浮点数乘法运算,我们都等要等满一个时钟周期。

在这个情况下,虽然 CPI 能够保持在 1,但是我们的时钟频率却没法太高。因为太高的话,有些复杂指令没有办法在一个时钟周期内运行完成。那么在下一个时钟周期到来,开始执行下一条指令的时候,前一条指令的执行结果可能还没有写入到寄存器里面。那下一条指令读取的数据就是不准确的,就会出现错误


指令流水线(Instruction Pipeline)

在取指令的时候

1.译码器把数据从内存里面取出来,写入到寄存器中

2.在指令译码的时候,我们需要另外一个译码器,把指令解析成对应的控制信号、内存地址和数据

3.到了指令执行的时候,我们需要的则是一个完成计算工作的 ALU。这些都是一个一个独立的组合逻辑电路

这样一来,我们就不用把时钟周期设置成整条指令执行的时间,而是拆分成完成这样的一个一个小步骤需要的时间。同时,每一个阶段的电路在完成对应的任务之后,也不需要等待整个指令执行完成,而是可以直接执行下一条指令的对应阶段

这里面每一个独立的步骤,我们就称之为流水线阶段(Pipeline Stage)

如果我们把一个指令拆分成“取指令 - 指令译码 - 执行指令”这样三个部分,那这就是一个三级的流水线。如果我们进一步把“执行指令”拆分成“ALU 计算(指令执行)- 内存访问 - 数据写回”,那么它就会变成一个五级的流水线

五级的流水线,就表示我们在同一个时钟周期里面,同时运行五条指令的不同阶段。这个时候,虽然执行一条指令的时钟周期变成了 5,但是我们可以把 CPU 的主频提得更高了。

我们不需要确保最复杂的那条指令在时钟周期里面执行完成,而只要保障一个最复杂的流水线级的操作,在一个时钟周期内完成就好了

如果某一个操作步骤的时间太长,我们就可以考虑把这个步骤,拆分成更多的步骤,让所有步骤需要执行的时间尽量都差不多长。这样,也就可以解决我们在单指令周期处理器中遇到的,性能瓶颈来自于最复杂的指令的问题。像我们现代的 ARM 或者 Intel 的 CPU,流水线级数都已经到了 14 级

虽然我们不能通过流水线,来减少单条指令执行的“延时”这个性能指标,但是,通过同时在执行多条指令的不同阶段,我们提升了 CPU 的“吞吐率”

流水线不能取太大,每一级流水线对应的输出,都要放到流水线寄存器(Pipeline Register)里面,然后在下一个时钟周期,交给下一个流水线级去处理

所以,设计合理的流水线级数也是现代 CPU 中非常重要的一点


流水线设计需要解决的三大冒险,分别是结构冒险(Structural Hazard)数据冒险(Data Hazard)以及控制冒险(Control Hazard)

结构冒险(Structural Hazard) 

假设在同一个时钟周期指令执行到访存阶段的时候,流水线里的其他指令,在执行取指令的操作。访存和取指令,都要进行内存数据的读取。我们的内存,只有一个地址译码器的作为地址输入,那就只能在一个时钟周期里面读取一条数据,没办法同时执行访存和取指令

解决办法

把高速缓存分成了指令缓存(Instruction Cache)和数据缓存(Data Cache)两部分,使得我们的 CPU 在进行数据访问和取指令的时候,不会再发生资源冲突的问题了


数据冒险(Data Hazard) 

其实就是同时在执行的多个指令之间,有数据依赖的情况。这些数据依赖,我们可以分成三大类,分别是先写后读(Read After Write,RAW)、先读后写(Write After Read,WAR)和写后再写(Write After Write,WAW)。

解决办法

我们需要有解决这些数据冒险的办法。其中最简单的一个办法,不过也是最笨的一个办法,就是流水线停顿(Pipeline Stall),或者叫流水线冒泡(Pipeline Bubbling)

时钟信号会不停地在 0 和 1 之前自动切换。其实,我们并没有办法真的停顿下来。流水线的每一个操作步骤必须要干点儿事情。所以,在实践过程中,我们并不是让流水线停下来,而是在执行后面的操作步骤前面,插入一个 NOP 操作,也就是执行一个其实什么都不干的操作


操作数前推(Operand Forwarding),或者操作数旁路(Operand Bypassing)

假设如下指令

add $t0, $s2,$s1
add $s2, $s1,$t0

所以后一条指令,需要等待前一条指令的数据写回阶段完成之后,才能执行

我们完全可以在第一条指令的执行阶段完成之后,直接将结果数据传输给到下一条指令的 ALU

在 CPU 的硬件里面,需要再单独拉一根信号传输的线路出来,使得 ALU 的计算结果,能够重新回到 ALU 的输入里来。这样的一条线路,就是我们的“旁路”



乱序执行(Out-of-Order Execution,OoOE)

比如如下代码

a = b + c
d = a * e
x = y * z

计算里面的 x ,却要等待 a 和 d 都计算完成,在流水线里,后面的指令不依赖前面的指令,那就不用等待前面的指令执行,它完全可以先执行。

1. 在取指令和指令译码的时候,乱序执行的 CPU 和其他使用流水线架构的 CPU 是一样的。它会一级一级顺序地进行取指令和指令译码的工作。

2. 在指令译码完成之后,就不一样了。CPU 不会直接进行指令执行,而是进行一次指令分发,把指令发到一个叫作保留站(Reservation Stations)的地方。

3.这些指令不会立刻执行,而要等待它们所依赖的数据,传递给它们之后才会执行

4. 一旦指令依赖的数据来齐了,指令就可以交到后面的功能单元(Function Unit,FU),其实就是 ALU,去执行了。我们有很多功能单元可以并行运行,但是不同的功能单元能够支持执行的指令并不相同

5. 指令执行的阶段完成之后,我们并不能立刻把结果写回到寄存器里面去,而是把结果再存放到一个叫作重排序缓冲区(Re-Order Buffer,ROB)的地方

6. 在重排序缓冲区里,我们的 CPU 会按照取指令的顺序,对指令的计算结果重新排序。只有排在前面的指令都已经完成了,才会提交指令,完成整个指令的运算结果

7. 实际的指令的计算结果数据,并不是直接写到内存或者高速缓存里,而是先写入存储缓冲区(Store Buffer) 面,最终才会写入到高速缓存和内存里



控制冒险(Control Hazard)

分支预测,最简单的分支预测技术,叫作“假装分支不发生”。顾名思义,自然就是仍然按照顺序,把指令往下执行,会有 50% 的正确率。

如果分支预测是正确的,我们自然赚到了。这个意味着,我们节省下来本来需要停顿下来等待的时间。

如果分支预测失败了呢?那我们就把后面已经取出指令已经执行的部分(因为后面要执行的指令都是提前读到高速缓存),给丢弃掉。这个丢弃的操作,在流水线里面,叫作 Zap 或者 Flush。CPU 不仅要执行后面的指令,对于这些已经在流水线里面执行到一半的指令,我们还需要做对应的清除操作。比如,清空已经使用的寄存器里面的数据等等,这些清除操作,也有一定的开销。

动态分支预测即如果前2次执行都是按照顺序而没发生跳转,那么认为下一次也是按照顺序往下执行,准确率会提高很多

void t1()
{
    int i,j,k;

    for (i = 0; i < 100; i++) {
        for (j = 0; j < 1000; j++) {
            for (k = 0; k < 10000; k++){

            }
        }
    }
}

void t2()
{
    int i,j,k;

    for (i = 0; i < 10000; i++) {
        for (j = 0; j < 1000; j++) {
            for (k = 0; k < 100; k++){

            }
        }
    }
}

上面2个循环,t1()分支预测错误会少很多


CPU 设计 - 多发射(Mulitple Issue)和超标量(Superscalar)

同一个时间,可能会同时把多条指令发射(Issue)到不同的译码器或者后续处理的流水线中去

在超标量的 CPU 里面,有很多条并行的流水线,而不是只有一条流水线。“超标量“这个词是说,本来我们在一个时钟周期里面,只能执行一个标量(Scalar)的运算。在多发射的情况下,我们就能够超越这个限制,同时进行多次计算



超线程(Hyper-Threading)技术

在一个物理 CPU 核心内部,会有双份的 PC 寄存器、指令寄存器乃至条件码寄存器。这样,这个 CPU 核心就可以维护两条并行的指令的状态。在外面看起来,似乎有两个逻辑层面的 CPU 在同时运行。所以,超线程技术一般也被叫作同时多线程(Simultaneous Multi-Threading,简称 SMT)技术

在 CPU 的其他功能组件上,Intel 可不会提供双份。无论是指令译码器还是 ALU,一个 CPU 核心仍然只有一份。因为超线程并不是真的去同时运行两个指令,那就真的变成物理多核了

超线程的目的,是在一个线程 A 的指令,在流水线里停顿的时候,让另外一个线程去执行指令。因为这个时候,CPU 的译码器和 ALU 就空出来了,那么另外一个线程 B,就可以拿来干自己需要的事情。这个线程 B 可没有对于线程 A 里面指令的关联和依赖。


SIMD 


SSE(Streaming SIMD Extensions)

在上面的 CPU 信息的图里面,你会看到,中间有一组信息叫作 Instructions,里面写了有 MMX、SSE 等等。这些信息就是这个 CPU 所支持的指令集。

这里的 MMX 和 SSE 的指令集,是一个提升 CPU 性能的技术方案,SIMD,中文叫作单指令多数据流(Single Instruction Multiple Data)

使用循环来一步一步计算的算法呢,一般被称为 SISD,也就是单指令单数据(Single Instruction Single Data)的处理方式。

如果你手头的是一个多核 CPU 呢,那么它同时处理多个指令的方式可以叫作 MIMD,也就是多指令多数据(Multiple Instruction Multiple Dataa)。

Intel 在引入 SSE 指令集的时候,在 CPU 里面添上了 8 个 128 Bits 的寄存器。128 Bits 也就是 16 Bytes ,也就是说,一个寄存器一次性可以加载 4 个整数。比起循环分别读取 4 次对应的数据,时间就省下来了

XMM0 XMM1 XMM2 XMM3 XMM4 XMM5 XMM6 XMM7


正是 SIMD 技术的出现,使得我们在 Pentium 时代的个人 PC,开始有了多媒体运算的能力。可以说,Intel 的 MMXSSE 指令集,和微软的 Windows 95 这样的图形界面操作系统,推动了 PC 快速进入家庭的历史进程


AVX (Advanced Vector Extensions)

高级向量扩展指令集 是x86架构微处理器中的指令集,由英特尔在2008年3月提出,并在2011年第一季度发布的Sandy Bridge系列处理器中首次支持。AMD在随后的2011年第三季度发布的Bulldozer系列处理器中开始支持AVX。

AVX是X86指令集的SSE延伸架构,把寄存器XMM 128bit提升至YMM 256bit,以增加一倍的运算效率。此架构支持了三运算指令(3-Operand Instructions),减少在编码上需要先复制才能运算的动作。在微码部分使用了LES LDS这两少用的指令作为延伸指令Prefix

AVX2指令集将大多数整数命令操作扩展到256位,并引入了熔合乘法累积(FMA)运算

ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7

xmm0 相当于 ymm0 的低16位

AVX-512则使用新的EVEX前缀编码将AVX指令进一步扩展到512位。Intel Xeon Scalable处理器支持AVX-512


具体细节可以参考:

X86_Assembly/SSE

Intel® AVX State Transitions: Migrating SSE Code to AVX

wiki-avx

上一篇: 浮点数与定点数
下一篇: cpu-异常
作者邮箱: 203328517@qq.com