CPU那些事儿

目录

一、 CPU的本质  

1.计算能力

2.记忆能力

3.寄存器

4.软件硬件

5.指令集

6.时钟信号

7.总结

二、CPU与机器指令

1.程序语言的演变

2.CPU的工作过程

3.寄存器

(1)通用寄存器

(2)程序计数器(rip寄存器,也叫PC寄存器、IP寄存器)

(3)栈指针寄存器

(4)状态寄存器

三、CPU执行指令 

1.CPU的工作模式

2.操作系统的作用

四、CPU 空闲时在干嘛?  

1.halt指令

2.系统空闲进程

五、CPU 是如何识数的?  

六、CPU 是如何理解 01 二进制的?  

七、CPU 是如何读写内存的? 

1.谁来告诉CPU读写内存

(1)精简指令集架构(RISC)

(2)复杂指令集架构

2.cpu不直接读取内存

3.程序局部性原理

4.缓存更新

(1)同步缓存更新

(2)异步缓存更新

5.多级cache

6.多核-cache一致性

7.cache

8.总结

八、复杂指令集 CISC  

1.cpu

2.复杂指令集

3.代码也是要占存储空间的

4.CPU真的在直接执行机器指令吗?

5.总结

九、精简指令集 RISC

1.精简指令集RISC

2.CISC与RISC对比(乘法运算)

3.精简指令集的优势

十、CPU分支预测 


一、 CPU的本质  

        生成万物的基础元素:与、或、非门。

1.计算能力

        类似通过与和异或可实现二进制加法,我们也可以根据需要通过与、或、非门将不同的算数运算设计出来。负责计算的电路有一个统称,这就是所谓的算术逻辑单元 arithmetic logic unit,ALU,CPU 中专门负责运算的模块,本质上和上面的简单电路没什么区别,就是更加复杂而已。

2.记忆能力

         现在你的电路能存储一个比特位了。

3.寄存器

        我们管这个组合电路就叫寄存器,你没有看错,我们常说的寄存器就是这个东西。你不满足,还要继续搭建更加复杂的电路以存储更多信息,同时提供寻址功能,就这样内存也诞生了。

        寄存器及内存都离不开上一节那个简单电路,只要通电,这个电路中就保存信息,但是断电后很显然保存的信息就丢掉了,现在你应该明白为什么内存在断电后就不能保存数据了吧。

4.软件硬件

        硬件不可变,但软件可变,不变的是硬件但提供不同的软件就能让硬件实现全新的功能,无比天才的思想,人类真的是太聪明了。

5.指令集

        这条指令占据 16 比特,其中前四个比特告诉 CPU 这是加法指令,这意味着该 CPU 的指令集中可以包含 2^4 也就是 16 个机器指令,这四个比特位告诉 CPU 该做什么,剩下的 bit 告诉 CPU 该怎么做,也就是把寄存器 R6 和寄存器 R2 中的值相加然后写到寄存器 R6 中。

6.时钟信号

        时钟信号每一次电压改变,整个电路中的各个寄存器(也就是整个电路的状态)会更新一下,这样我们就能确保整个电路协同工作不会这里提到的问题。

        现在你应该知道 CPU 的主频是什么意思了吧,主频是说一秒钟指挥棒挥动了多少次,显然主频越高 CPU 在一秒内完成的操作也就越多。

7.总结

        可以完成各种计算的 ALU、可以存储信息的寄存器以及控制它们协同工作的时钟信号,这些统称 Central Processing Unit,简称就是 CPU。

二、CPU与机器指令

        CPU是计算机的大脑,程序员写的代码最终都是CPU来执行的。但作为计算机的大脑,CPU并不认识C,C++、Python、Java等语言,这些语言是人类可以认识的,CPU真正能理解的是机器指令。

1.程序语言的演变

        01指令 ->汇编语言 -> C语言

2.CPU的工作过程

        CPU要执行的指令是存放在内存中的。可执行程序一般保存在磁盘当中,运行时需要拷贝到内存,这被称之为程序加载。然后操作系统告诉CPU机器指令所在内存的起始地址,然后CPU从该地址开始执行我们写的程序。

CPU的工作过程就是这样的:

  1. 从内存中取出机器指令
  2. 对指令进行解码
  3. 执行指令,执行完毕后回到1

3.寄存器

        寄存器(很贵)是一种制作在CPU中的高速存储介质。本质上寄存器和内存是一样的,都是存放0或1的盒子,但是寄存器读写非常快,同时容量相对于内存小很多,现在内存容量通常已经达到GB,而寄存器容量通常是MB甚至KB。

        只有汇编语言才能操作寄存器,高级语言比如C/C++、Java都不能直接对寄存器进行编程。

        x86-64中,所有寄存器都是64位,相对32位的x86来说,标识符发生了变化,比如:从原来的 %ebp 变成了 %rbp。为了向后兼容性,%ebp 依然可以使用,不过指向了 %rbp 的低32位。

(1)通用寄存器

  rbp 栈基地址寄存器,保存当前帧的栈底地址。

  rsp 栈指针寄存器,保存当前帧的栈顶地址。

(2)程序计数器(rip寄存器,也叫PC寄存器、IP寄存器)

  rip 指令地址寄存器(指令指针),用来存储 CPU 即将要执行的指令的内存地址。每次 CPU 执行完相应的汇编指令之后,rip 寄存器的值就会自行累加;rip 无法直接赋值,call, ret, jmp 等指令可以修改 rip

(3)栈指针寄存器

        函数调用时会创建栈帧来保存调用函数时的参数以及函数运行过程定义的局部变量。这些栈帧是以先进后出的形式在内存中进行分配释放的,那么CPU是如何能知道当前栈帧在内存中的哪个位置上?

        跟踪调用栈是通过CPU中叫做栈指针stack pointer的寄存器来完成的(注意这里的指针不是C语言中的指针,这里的指针指的是寄存器)。

        任何时候,只要通过查看CPU的栈指针,我们就能知道进程中当前正在被执行函数的栈帧在内存中的位置。

(4)状态寄存器

        程序的执行一般有两种模式:内核态和用户态。

        那么我们怎么知道当前程序运行在哪种状态呢?答案就在CPU内部的状态寄存器中,该寄存器中有特定的比特位来标记当前CPU正工作在哪种模式下。

三、CPU执行指令 

1.CPU的工作模式

        CPU是通过执行机器指令来来处理任务的,我们的程序(用户程序)会被编译成机器指令,操作系统同样需要编译成机器指令,不要忘了操作系统也是一个大程序,CPU通过运行操作系统来控制整个计算机系统。

        原来CPU可以执行的指令从等级上讲有两类,一类是“特权指令”,这类指令多涉及硬件操作,另一类是普通指令。CPU在执行指令时工作在两种模式下:“内核模式(Kernel mode)”以及“用户模式(User mode)”。CPU在用户模式下只能执行部分整个指令集的一部分,比如在用户模式下不能执行与I/O有关的指令,不能访问整个内存的地址空间,不可以执行特权指令。但是在内核模式下,CPU可以执行其指令集架构允许的任何机器指令包括特权指令,可以访问所有的内存地址,可以执行I/O操作等等。

        当执行用户程序时(也就是我们写的程序时),CPU工作在用户模式,这时,CPU的操作是受限的,也就是说用户程序是受到限制的,用户程序不可以任意访问内存,不可以直接向I/O设备发起请求等等。当CPU开始运行操作系统时,CPU工作在内核模式下,这时,CPU可以执行任意操作,操作系统充分信任自己,因此操作系统对自己不会施加限制。

        通过系统调用CPU可以从执行用户程序的用户模式转移到执行操作系统的内核模式。这也是普通用户程序向操作系统发起请求的唯一合法途径。

2.操作系统的作用

        CPU每次只能执行一个程序(进程),但是有了操作系统情况就不太一样了,操作系统的一项魔法就是让多个程序(进程)同时运行,每个运行中的进程都觉得自己独占CPU。

操作系统拥有两项魔法:

  • 让每个进程都觉得自己独占CPU
  • 让每个进程都觉得自己独占内存(虚拟内存)

四、CPU 空闲时在干嘛?  

1.halt指令

        halt 指令是特权指令,也就是说只有在内核态下 CPU 才可以执行这条指令,程序员写的应用都运行在用户态,因此你没有办法在用户态让 CPU 去执行这条指令。

        不要把进程挂起和 halt 指令混淆,当我们调用 sleep 之类函数时,暂停运行的只是进程,此时如果还有其它进程可以运行那么 CPU 是不会空闲下来的,当 CPU 开始执行halt指令时就意味着系统中所有进程都已经暂停运行。

2.系统空闲进程

 

       计算机中存在一个“系统空闲进程”,“系统空闲进程”使用的CPU资源就是未使用的CPU资源。如果程序使用了5%的CPU,则系统空闲进程将使用95%的CPU。 可以将其视为简单的占位符, 这就是为什么任务管理器将此过程描述为“处理器空闲时间的百分比”。 PID(进程标识符)为0。

        系统空闲进程的唯一目的是使CPU在等待下一个计算或进程进入时忙于做任何事情(实际上是任何事情)。 所有这些工作的原因是,空闲线程使用零优先级,该优先级低于普通线程,因此允许它们在操作系统运行合法进程时被从队列中推出。 然后,一旦CPU完成该工作,就可以再次处理系统空闲进程。 使空闲线程始终处于“就绪”状态(如果尚未运行),会使CPU处于运行状态,并等待操作系统对其进行处理。

        调度器在没有其它进程可供调度时就开始运行空闲进程,也就是在循环中不断的执行 halt 指令,此时 CPU 开始进入低功耗状态。

        在 Linux 内核中,这段代码是这样写的:

while (1) {
  ...
  while(!need_resched()) {
      cpuidle_idle_call();
      ...
  }
}

        其中 cpuidle_idle_call函数最终会执行 halt 指令,注意,这里删掉了很多细节,只保留最核心代码。

        计算机系统空闲时 CPU 在干嘛,就是在执行这一段代码,本质上就是 CPU 在执行 halt 指令。

五、CPU 是如何识数的?  

        最左边的 bit 位是 0 则表示正数,否则表示负数。

  • 0******* 正数
  • 1******* 负数

        原码、反码解决不了2+(-2)=0的问题,而补码可以。负数的反码加上1就是对应的补码。

        CPU其实本质的上是不识数的,也不需要识数。至于数字该采用反码还是补码这些是人类需要理解的,确切来说是编译器需要来理解的,程序员都无需关心,但程序员需要知道数据类型的表示范围

六、 

        CPU不认识也不理解任何东西

        硬件感知到的仅仅就是电压。电压有两种,高电压和低电压。这就是01二进制,高电压代表1低电压代表0,0和1仅仅是人类可以理解的东西,硬件电路可不理解这玩意,它仅仅就是靠电流驱动来工作。

        CPU根本不认识任何语言,理解编程语言的其实是编译器。计算机所谓能理解二进制就好比你的台灯能理解开关一样。

七、CPU 是如何读写内存的? 

1.谁来告诉CPU读写内存

        程序员通过高级语言编写程序,编译器将其翻译为机器指令,机器指令来告诉CPU去读写内存。

(1)精简指令集架构(RISC)

        在精简指令集架构(RISC)下会有特定的机器指令,Load/Store指令来读写内存.

        精简指令集下,一条机器指令操作的数据必须来存放在寄存器中,不能直接操作内存数据,因此RISC下,数据必须先从内存搬运到寄存器,这就是为什么RISC下会有特定的Load/Store访存指令。

(2)复杂指令集架构

        以x86为代表的复杂指令集架构下没有特定的访存指令。一条机器指令操作的数据可以来自于寄存器也可以来自内存,因此这样一条机器指令在执行过程中会首先从内存中读取数据。

        不管是RISC下特定的Load/Store指令还是x86下包含在一条指令内部的访存操作,这里读写的都是内存中的数据,除此之外还要意识到,CPU除了从内存中读写数据外,还要从内存中读取下一条要执行的机器指令。

所以,CPU读写内存其实是由两个因素来驱动的:

  1. 程序执行过程中需要读写来自内存中的数据。
  2. CPU需要访问内存读取下一条要执行的机器指令。

2.cpu不直接读取内存

        cpu跑的很快,相比之下,内存读取就很慢。在这种速度差异下,CPU执行一条涉及内存读写指令时需要等“很长一段时间“数据才能”缓缓的“从内存读取到CPU中,所以CPU不直接读写内存。

3.程序局部性原理

        如果我们访问内存中的一个数据A,那么很有可能接下来再次访问到,同时还很有可能访问与数据A相邻的数据B,这分别叫做时间局部性空间局部性

        程序占据的内存空间只有一少部分在程序执行过程经常用到。我们将这常用的部分集中起来,放到一种比内存速度更快的存储介质上,这种介质就是我们熟悉的SRAM (Static RAM,静态随机存储器),普通内存一般是DRAM(动态随机存储器),这种读写速度更快的介质充当CPU和内存之间的Cache,这就是所谓的缓存。

  • SRAM成本比较高,DRAM成本较低(1个场效应管加一个电容
  • SRAM存取速度比较快,DRAM存取速度较慢(电容充放电时间)
  • SRAM一般用在高速缓存中,DRAM一般用在内存条里

        把经常用到的数据放到cache中存储,CPU访问内存时首先查找cache,如果能找到,直接返回即可,找不到再去查找内存并更新cache。我们可以看到,有了cache,CPU不再直接与内存打交道了

        cache的快速读写能力是有代价的,代价就是Money,造价不菲,因此我们不能把内存完全替换成cache的SRAM,那样的计算机你我都是买不起的

        因此cache的容量不会很大,但由于程序局部性原理,因此很小的cache也能有很高的命中率,从而带来性能的极大提升。

4.缓存更新

        有了cache,CPU不再直接与内存打交道,因此CPU直接写cache,但此时就会有一个问题,那就是cache中的值更新了,但内存中的值还是旧的,这就是所谓的不一致问题,inconsistent。如何解决呢?

(1)同步缓存更新

        最简单的方法是这样的,当我们更新cache时一并把内存也更新了,这种方法被称为 write-through,很形象吧。

        可是如果当CPU写cache时,cache中没有相应的内存数据该怎么呢?这就有点麻烦了,首先我们需要把该数据从内存加载到cache中,然后更新cache,再然后更新内存。

(2)异步缓存更新

        异步的这种方法是这样的,当CPU写内存时,直接更新cache,然后,注意,更新完cache后CPU就可以认为写内存的操作已经完成了,尽管此时内存中保存的还是旧数据。

        当包含该数据的cache块被剔除时(当cache已满时,增加一项新的数据就要剔除一项旧的数据)再更新到内存中,这样CPU更新cache与更新内存就解耦了,也就是说,CPU更新cache后不再等待内存更新,这就是异步,这种方案也被称之为write-back,这种方案相比write-through来说更复杂,但很显然,性能会更好。

5.多级cache

        现代CPU为了增加CPU读写内存性能,已经在CPU和内存之间增加了多级cache,典型的有三级,L1、L2和L3,CPU读内存时首先从L1 cache找起,能找到直接返回,否则就要在L2 cache中找,L2 cache中找不到就要到L3 cache中找,还找不到就不得不访问内存了。

        现代计算机系统CPU和内存之间其实是有一个cache的层级结构的

        越往上,存储介质速度越快,造价越高容量也越小;越往下,存储介质速度越慢,造价越低但容量也越大。

        注意:要想获得极致性能是有前提的,那就是程序员写的程序必须具有良好的局部性,充分利用缓存。

6.多核-cache一致性

        拥有一堆核心的CPU其实是没什么用的,关键需要有配套的多线程程序才能真正发挥多核的威力。如果一个cache中待更新的变量同样存在于其它核心的cache,那么你需要一并将其它cache也更新好。

        so,现代计算机中CPU和内存之间有多级cache,CPU读写内存时不但要维护cache和内存的一致性,同样需要维护多核间cache的一致性

7.cache

        CPU读内存时首先从L1 cache找起,能找到直接返回,否则就要在L2 cache中找,L2 cache中找不到就要到L3 cache中找,还找不到就不得不访问内存了,到查内存时还不算完,现在有了虚拟内存,内存其实也是一层cache,是磁盘的cache,也就是说查内存也有可能不会命中,因为内存中的数据可能被虚拟内存系统放到磁盘中了,如果内存也不能命中就要查磁盘

8.总结

        CPU读写内存非常简单吗?这一过程涉及到的硬件以及硬件逻辑包括:L1 cache、L2 cache、L3 cache、多核缓存一致性协议、MMU、内存、磁盘;软件主要包括操作系统。

八、复杂指令集 CISC  

1.cpu

        中央处理器(CPU),是电子计算机的主要设备之一,电脑中的核心配件。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU是计算机中负责读取指令,对指令译码并执行指令的核心部件。中央处理器主要包括两个部分,即 控制器运算器 ,其中还包括 高速缓冲存储器 及实现它们之间联系的数据、控制的总线。电子计算机三大核心部件就是CPU、内部存储器、输入/输出设备。

CPU:执行一条条指令。

指令集: Instruction Set Architecture ,ISA。指令集中包含各种各样的指令:

  • 会加法
  • 会从内存把数据搬运到寄存器
  • 会跳转
  • 会比较大小...

2.复杂指令集

        复杂指令集,Complex Instruction Set Computer,简称CISC。当今普遍存在于桌面PC以及服务器端的x86架构就是基于复杂指令集CISC,生产x86处理器的厂商就是我们熟悉的“等,等等等等”英特尔以及AMD。

        大家认为高级语言中的一些概念比如函数调用、循环控制、复杂的寻址模式、数据结构和数组的访问等都应该直接有对应的机器指令,这些就是现代大家认为的复杂指令集CISC非常鲜明的特点。

        基于对程序员方便编写汇编语言以及节省代码存储空间的需要,直接促成了复杂指令集的设计。

3.代码也是要占存储空间的

        可执行程序中,比如Windows下的EXE或者Linux下的ELF文件,即包含机器指令也包含数据,对于程序员来说我们可以简单的认为可执行程序中有两部分内容:数据段以及代码段:

4.CPU真的在直接执行机器指令吗?

        一般我们认为CPU直接执行机器指令,严格来说这是不正确的,对于含有微代码设计的CPU来说,CPU直接执行的并不是机器指令,而是微代码,微代码是CPU以及机器指令的中间层。

5.总结

        CPU是整个计算机系统的核心,CPU指令集ISA更是核心中的核心。

        本文从历史的角度讲述了复杂指令集出现的必然,复杂指令集对于那些直接使用汇编语言进行编程的程序员来说是很方便的,同时复杂指令集的指令密度更高,相同的存储空间可以存储更多程序,这一切都推动了复杂指令集的发展。

九、精简指令集 RISC

        大概80%的时间CPU都在执行那20%的机器指令,同时CISC中一部分比较复杂的指令并不怎么被经常用到,而且那些设计编译器的程序员也更倾向于组合一些简单的指令来完成特定任务。

        复杂指令集中那些被认为可以提高性能的指令其实在内部被微代码拖后腿了,如果移除掉微代码,程序反而可以运行的更快,并且可以节省构造CPU消耗的晶体管数量。

1.精简指令集RISC

        有了简单指令CPU内部的微代码也不需要了,没有了微代码这层中间抽象,编译器生成的机器指令对CPU的控制力大大增强,有什么问题让写编译器的那帮家伙修复就好了,显然调试编译器这种软件要比调试CPU这种硬件要简单很多。

        在复杂指令集下,一条机器指令可能涉及到从内存中取出数据、执行一些操作比如加和、然后再把执行结果写回到内存中,注意这是在一条机器指令下完成的。

        但在精简指令集下,这绝对是大写的禁忌,精简指令集下的指令只能操作寄存器中的数据,不可以直接操作内存中的数据,也就是说这些指令比如加法指令不会去访问内存。原来在精简指令集下有专用的 load 和 store 两条机器指令来负责内存的读写,其它指令只能操作CPU内部的寄存器,这是和复杂指令集一个很鲜明的区别。

2.CISC与RISC对比(乘法运算)

复杂指令集

MULT A  B

虽然是一条机器指令,但是背后很复杂。

背后逻辑:

  1. 从内存中加载地址A上的数,存放在寄存器中
  2. 从内存中夹杂地址B上的数,存放在寄存器中
  3. ALU根据寄存器中的值进行乘积
  4. 将乘积写回内存

精简指令集

        RISC更倾向于使用一系列简单的指令来完成一项任务,我们来看下一条MULT指令需要完成的操作:

  1. 从内存中加载地址A上的数,存放在寄存器中
  2. 从内存中夹杂地址B上的数,存放在寄存器中
  3. ALU根据寄存器中的值进行乘积
  4. 将乘积写回内存

        这几步需要a)从内存中读数据;b)乘积;c) 向内存中写数据,因此在RISC下会有对应的LOAD、PROD、STORE指令来分别完成这几个操作。

        Load指令会将数据从内存搬到寄存器;PROD指令会计算两个寄存器中数字的乘积;Store指令把寄存器中的数据写回内存,因此如果一个程序员想完成上述任务就需要写这些汇编指令:

LOAD RA, A
LOAD RB, B
PROD RA, RB
STORE A, RA

        这些指令都非常简单,CPU内部不需要复杂的硬件逻辑来进行解码,因此更节省晶体管,这些节省下来的晶体管可用于其它功能上。

3.精简指令集的优势

        同样一项任务,在CISC下只需要一条机器指令,而在RISC下需要四条机器指令,显然RISC下的程序本身所占据的空间要比CISC大,而且这对直接用汇编语言来写程序的程序员来说是很不友好的,因为更繁琐嘛!但RISC设计的初衷也不是让程序员直接使用汇编语言来写程序,而是把这项任务交给编译器,让编译器来生成机器指令。

        RISC中每条指令更加简单,执行时间比较标准,因此可以很高效的利用流水线技术,这一切都让采用RISC架构的CPU获得了很好性能。

十、CPU分支预测 

        流水线:处理一条机器指令可以分为几个步骤:取指、译码、执行、回写......,这几个阶段分别由特定的硬件来完成 (注意,真实 CPU 内部可能会将执行一条指令分解为数十个阶段)

        程序员在代码中编写的 if 语句一般会被编译器翻译成一条跳转指令,if 语句其实起到一种分支的作用,如果条件成立则需要执行if内部的逻辑,否则不执行;因此跳转指令会依赖自身的执行结果来决定到底要不要跳转,这会对流水线产生影响。

        跳转指令需要依赖自身的执行结果来决定到底要不要跳转,那么在跳转指令没有执行完的情况下 CPU 怎么知道后面哪个分支的指令能进入到流水线呢分支预测。

        如果 CPU 猜的不对,那么流水线上的后续指令将作废,这就解释了为什么处理有序数组要比处理无序数组性能好了,因为在数组有序的情况下,CPU 的分支预测几乎不会猜错,流水线上的指令不会被频繁作废。如果你编写了 if 语句,那么你最好让 CPU 大概率能猜对

        实际上现代 CPU 的分支预测是很聪明的,对于非核心部分的if 语句分支预测失败带来的性能损失可以忽略不计。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烫青菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值