从计组角度去看如何优化指令执行流程及线程进程区别

2022-10-09 13:05:18 浏览数 (1)

theme: condensed-night-purple

回顾

早期计算机的提速方式是 减少晶体管的切换时间。晶体管组成了逻辑门,ALU和之前讲的其他组件,但是这种方式最终会遇到瓶颈,所以处理器厂商发明各种新技术来提升性能,不但让简单指令运行更快也让他能进行更复杂的运算

硬件层面的优化

上一篇文章中我们做了一个除法的程序,其实现原理是不断使用减法当检测到0或者负数时停止。但是这种方式需要很多个时钟周期才能完成(13个周期,每次发一个时钟周期CPU去取一次指令执行【取指令,解码,执行】),特别低效。

现在的CPU已经直接再硬件层面设计了除法,可以直接给ALU除法指令让其运行除法,只需一个时钟周期。

聪明的你应该已经想到了现代CPU厂商提高效率的办法了:没错,那就是给硬件设计越来越多的指令让其支持更多的操作,只为了减少执行操作时所需要的时钟周期。 (除法之前再软件层面设计基于简单的硬件指令需要执行13个,现在只需要一个指令即可)。

现代处理器中有专门的电路来处理图形操作,解码压缩视频,加密文档。。。

跑车没油了?

通过上面的优化操作,指令执行的效率越来越快了,但是指令执行需要的是数据。因此出现了另外一个问题——如何快速传递数据给CPU? 就像跑车的速度很快,但你加油的速度太慢了。你老是得等油加满才能继续跑,所以解决问题时怎样让油加的更快

RAM是CPU之外的独立组件,前面几篇文章我们也讲过指令是存储在RAM中的,CPU需要去和RAM通信,包括从RAM中取指令,读取指定内存地址的值给寄存器,寄存器将值存储到指定内存地址。。。。这些操作都是通过BUS总线来通信的(BUS总线结合了之前的RAM的允许数据输入线,数据输出线等等)

总线可能只有几厘米,电信号传输接近光速,因此即使很小的延迟也会造成问题。比如RAM需要时间去找数据,输出数据,一个从内存中读数据这个指令可能要多个时钟周期去完成,但是此时CPU却在空等数据这肯定是不行的。

解决的方法之一就是给CPU加一点RAM,也就是“缓存”

CPU建立缓存

处理器的空间不大,所以缓存只有KB或者MB,而RAM都是GB,缓存提高了速度。

缓存行

如果每次CPU从RAM读取数据时RAM传的不是一个,而是一块呢?

我们假设现在是一个图书管理员,需要将刚采购的图书放到指定类别的图书架上。大家不可能一次只拿一本去挨个找到对应的货架放上去吧,都是一次拿很多本然后再去放。虽然这样拿书花的时间会长一点,但是这些书都在你手上可以很快的把书放到货架上,而不是每次都去拿一本。

和每次读取一个缓存行的道理是一样的,虽然读取的时间会花的久一点,但是数据可以存在缓存中。 当取数据时如果缓存中存在这块数据就叫缓存命中,缓存中不存在就叫缓存未命中。 对比于刚刚图书管理员的例子,如果你手上拿的书正好是货架对应类别的那就可以直接放到架子上,否则还是需要去采购的图书里面再拿下一批

缓存离CPU近,一个时钟周期就可以给到数据,CPU不需要空等数据了。

缓存不一致-脏位

缓存也可以当临时空间,存一些中间值,适合长/复杂的运算

有了缓存当然方便,我们计算出来的值也可以放入到缓存中这样就省的放入内存再从内存中读取了,但是好是好,有一个问题就是缓存中的数据会和RAM中不一致。这种不一致会被记录下来:在每个缓存行的空间中都有一个特殊标记,叫“脏位”, 等待之后同步到内存中

同步-将脏位写回内存

同步发生的时机一般是在缓存满了但是CPU又要缓存时,在清理缓存腾出空间之前,先检查“脏位”,如果数据是脏的,在放置新缓存时,将脏数据写回到内存中

指令流水线

洗衣机例子

我们可以并行处理进一步提高效率。

来看下面这个例子: 绿色的是洗衣机,紫色的是烘干机。 洗一套衣服然后烘干需要的时间是一小时,以此类推烘干完成在洗下一批衣服

我们换个思路和之前一样先把第一批衣服先放进洗衣机,洗完之后放到烘干机中。此时的洗衣机是空闲的,因此我们可以再放一批衣服到洗衣机中,当第一批烘干之后第二批的衣服洗完了也开始烘干了,此时第三批衣服扔到洗衣机中。通过充分利用空闲间隔并行处理可提高效率

处理器也可以按照这样的设计进行处理程序。

CPU能否像洗衣机那样并行处理

CPU执行一条指令也是类似的操作:取址-》解码-》执行,不断重复。此时一条指令需要三个时钟周期才能完成(取址,解码,执行)。

并且这三个步骤用的都是CPU的不同的部分,取址是指令地址寄存器和指令寄存器做的,解码是控制单元做的,执行是ALU做的。 和洗衣机的例子很像,洗衣机和烘干机可以充分利用间隔,CPU也同样可以利用其他部件间隔去执行操作

也即执行一个指令的时候同时去解码下一个指令操作,读取下下一个指令:

经过并行处理优化后的CPU现在一个时钟周期就可以执行一条指令了。而不是之前必须得等取址和解码完成后在进行执行操作:

指令依赖问题-乱序执行

就像洗衣机的烘干机一样,在烘干机之前必须要有洗好的衣服才能进行接下来的烘干操作,当步调不同步时就会发生错误,而且烘干机依赖洗衣机洗出的衣服这个依赖关系很明显;CPU也是一样的,比如当前正在读取某个数据(解码阶段读取内存或者寄存器的值),而此时的执行阶段却在修改这个值(ALU此时正在修改值),也就是说拿到的是旧数据。

因此流水线处理器,要先弄清楚数据依赖性,必要时停止流水线,避免出现问题。

高端的CPU处理器会更近一步,动态排序有依赖关系的指令,最小化流水线的停工时间,这叫做“乱序执行”

有条件跳转指令流-推测执行,分支预测

简单的流水线处理器看到JUMP指令会停一会儿需要等待条件值计算出来判断条件是否满足,在继续执行流水线。

推测执行

因为这部分也有等待,所以高端处理器会使用一些技巧优化:

可以把JUMP看成“岔路口”,高端CPU会猜哪条路的可能性大一些,然后提前把指令放进流水线,这叫“推测执行”。

如果CPU猜对了,由于流水线已经塞满了正确的指令因此可以马上运行;如果猜错了,就要清空流水线。

分支预测

CPU厂商开发了复杂的方法来尽可能减少情况流水线的次数,这叫“分支预测”,现在CPU正确率超过百分之九十。

添加相同电路让一个时钟周期可以处理多个指令

虽然流水线已经避免了某些部件空闲,但是仍有些区域可能还是空闲的,比如从内存取值这个指令的执行过程中alu就会没有事情做。所以一次性处理多条指令会更好****

随着“超标量处理器”的出现使得一个时钟周期可以完成多个指令:

  1. 一次性处理多条指令(取指令 解码)

无非就是在加几个一模一样的电路:比如两个ALU两个加法运算可以同时运行,不需要等第一个运行完了才运行第二个。这种方式简单粗暴

  1. 如果利用的是CPU不同的组件,那么可以同时执行

比如内存取指令就可以和ALU计算这两个执行阶段的操作同时运行

我们再优化一下:

在原先的电路中多加几个相同的电路执行出现频次很高的指令,比如cpu中有四个或者八个完全相同的alu,可同时执行多个运算。如下图:

多核

上面所说的都是如何优化一条指令执行的速度(比如并发,乱序,分支预测,加相同电路让某个频繁操作可以同时进行处理),另外一种提升性能的方式就是 同时运行多个指令流,使用多核处理器:

可以看到就是将原先的独立处理单元,复制了很多块,这些就是多核

注意这个时候进城和线程区别就来了

平常说的四核两核处理器指的是一个cpu里面,有多少个独立处理单元:核

一个核代表可以运行一个线程,之后讲解线程说明

三级缓存

由于这些处理单元非常紧密在一个cpu中,因此这些核可以共享一些资源,比如缓存可以使多个核合作运算,多个核之间共享的缓存是l3缓存。l2和l1是核内的缓存,且l3.l2.l1缓存的内容越来越少。

进程

进程是一段程序比如QQ,微信这些程序,这些程序呢运行肯定需要指令,这些指令存储在什么地方呢?存储在RAM中,也就是所谓的把程序装入内存。因此创建进程需要申请内存空间,把这个进程的指令装入内存中。进程是静态的,是用来申请存储空间和其他资源(比如网络)的。

进程放入内存中时,会默认有一个主线程去运行指令

进程上下文切换

CPU采用时间片轮转的机制来运行进程,这个进程运行一会,那个进程运行一会。进程切换的时候需要保存上下文信息:不仅包括程序运行位置(指令地址寄存器),虚拟内存(程序指令空间)、栈、全局变量等用户空间资源,还包括内核堆栈、寄存器等内核空间状态。 上下文切换过程中的信息被保存在进程控制块(PCB-Process Control Block)中。PCB又被称作切换帧(SwitchFrame)。上下文切换的信息会一直被保存在CPU的内存中,直到被再次使用。 每次进程上下文切换需要几十纳秒到数微秒的CPU时间. 并且Linux通过TLB来管理虚拟内存到物理内存之间的映射,当虚拟内存更新后,TLB也需要刷新,内存的访问也会随之变慢。特别在多核处理器系统上,缓存被多个处理器共享,刷新缓存不仅会影响当前核处理器的进程,还会影响共享缓存(也即L3缓存)的其他核处理器的进程。

流程

进程1运行到指令2的时候,分配给进程1的时间片到了,此时该运行进城2了,把当前进程1的上下文数据保存到当前进程内存的PCB区域;接着恢复进程2地址空间中上次保存的PCB数据运行,也就是读取进程2内存的PCB区域赋值给寄存器,栈 再运行。

当进程2的时间片到了之后也是一样 保存进程2的TCB,恢复进程1的TCB, 上次记录的是运行到指令2了,因此下一个指令是运行指令3

线程

线程是用来运行进程中的指令的,也就是真正执行运算的。数据存放在进程中,线程会去取数据然后利用ALU,寄存器这些计算存储单元去运行进程中的指令。线程有自己的ALU,指令地址寄存器和缓存,上面提到过核和CPU是挨在一起的。

线程上下文切换

接下来把线程可以理解成就是核

如果某个指令比较耗时,那么程序就会卡住

比如指令1和2是IO,指令三四是简单的计算。通过拆分线程1执行指令一二,线程2去做指令三四可以大大提高程序执行效率

所以我们需要有多个线程去运行,因此线程是瓜分进程内存指令运行的,这个线程运行一会,那个线程运行一会。所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。

线程也有自己的私有数据,比如栈和寄存器,当前运行到哪个地方了等,这些在上下文切换时也是需要保存的。线程的上下文切换其实就可以分为两种情况: 两个线程属于不同进程,因为资源不共享,切换过程和切换进程上下文一样 两个线程属于同一个进程,只需要切换线程的私有数据、寄存器等不共享的数据,内存地址等还是共享的同一个进程的。

流程

假设Core1运行线程1,Core2运行线程2;线程1运行指令1,2;线程2运行指令3,4~~~

~线程1运行完指令1的时候内核发生调度当前运行这个线程的核Core1保存线程1的运行位置寄存器栈等数据;然后线程2执行完指令3的时候同样也是将数据保存到Core2中。 此时切换到Core1执行的时候保存当前执行的Core2数据,恢复取出Core1上次保存的信息也就是执行指令2,执行线程2的时候也是一样的,保存Core1信息,恢复Core2执行指令4。 可以看到和进程的上下文切换对比,线程切换更轻量,保存的上下文信息更少,也不需要保存到TCB内存区域中。(量少速度快)

线程是一个独立可执行单元,也就是CPU内部的核,我们总说四核处理器八核处理器,意思就是一个CPU内部可以运行多少个线程,一个核运行一个线程,上面也说过核就是一个独立可执行单元,他有自己的计算单元ALU,缓存还有指令地址寄存器等等。

单核双线程技术:一般来说一个核是一个线程,但是现在可以做到一个核可以运行两个线程,他的本质大家可以猜想下:核的内部有两个线程运行,和CPU运行多个核的道理一样。也是核内部运行这个线程一会,运行那个线程一会,有单独的空间去存储线程运行到哪个地方了。

线程越多越好吗?

此处分析的是单CPU多核的情况,如果是多CPU架构的可以做到真正的并行运行多个线程会比单CPU只是切换调度实现并发的更好。

并不是,线程的运行看起来是并行的,但是底层其实是CPU在分配时间片让每个进程(其实真正做运算的是线程只不过是从宏观上考虑多个进程也即多个线程)都可以得到执行(默认主线程)。当切换的时候需要保存好当前线程的上下文信息,再进行切换。如果线程过多比如几万个即使每次切换现场都是很快也会有很大的时间浪费在保存上下文上; 除此之外如果是不同进程的线程,每次切换都需要将进程的所有数据保存到PCB中,耗时比同一个进程的线程切换更严重。

总结

最后总结下:

  1. 指令运行速度快了,但是依赖的数据却需要很长时间才能获取到,因此我们在CPU内部设立了缓存,因为缓存存在一致性问题,因此又有了脏位
  2. 每次执行指令都需要经过固定的三个步骤取指令,解码,执行,这三个步骤执行完成就完成了一条指令的执行,但是这三个步骤其实是可以串起来的,因此我们缩短到一个时钟周期就可以完成一个指令
  3. 如果当前指令的执行阶段和下一个指令的解码阶段是有依赖的那么就会发生错误,因此计算机需要识别指令依赖问题;然后我们又对分支预测做了优化
  4. 但是还是太慢,执行阶段其实有些部分还是空闲的,因此对于某些依赖不同部件的执行指令可以同时运行,并且多加几个相同电路让频繁执行的指令减少浪费时间在等待上
  5. 当某些指令比较耗时的话会影响其他指令的运行CPU出了乱序执行优化指令顺序;
  6. 上面的优化都是在对一条指令执行速度的优化,如果我们可以同时运行多个指令呢? 一个核是一个独立的执行单元也即每次只能运行一条指令(我们之前的优化就是对一个核运行的更快速高效)。
  7. 多核处理器,每次可以并发运行多个指令(同一时间还是只有一个线程运行,切换核的速度特别快可以几乎不计)让程序的执行更加高效
  8. 现在都是多个CPU多核了,可以真正实现并行运行程序(同一时间多个程序同时运行)

到此计组的东西就看完了,之后是编程语言的发展了,可以移步二进制到高级语言的编码历史查看,读者有推荐的计组视频也可以留在评论区,感谢~

参考链接

CPU 上下文切换、用户态、内核态、进程与线程上下文切换

Linux 进程、线程和CPU的关系,cpu亲和性

原文视频

【计算机科学速成课】[40集全/精校] - Crash Course ComputerScience

Youtube 原视频

0 人点赞