Skip to content

1. CPU 执行程序的基本过程

程序实际上是一条一条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是 CPU 了。

CPU 执行程序的过程如下:

  1. CPU 读取【程序计数器】的值,这个值是指令的内存地址,CPU 的【控制单元】操作【地址总线】指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好了之后通过【数据总线】将指令数据传给CPU,CPU收到后,将指令数据存入到【指令寄存器】。
  2. 【程序技术器】的值自增,表示指向下一条指令。这个自增的大小由 CPU 的位宽决定,比如 32 位的CPU,指令大小是 4 个字节,那么需要4 个字节的内存地址存放,因此自增的值就是 4
  3. CPU 分析【指令寄存器】中的指令,确定指令的类型和参数,如果是计算类型的指令,交给【逻辑运算单元】运算;如果是存储类型指令,交给【控制】单元执行。

1.1 a = 1 + 2 执行的具体过程

CPU 不认识 a = 1 + 2 这个字符串,这些字符串只是方便我们程序员认识,要想这段程序能跑起来,还需要把整个程序翻译成汇编语言的程序,这个过程称为编译成汇编代码。

针对汇编代码,我们还需要用汇编器翻译成机器码,这些机器码由 0 和 1 组成的机器语言,这一条条机器码,就是一条条的计算机指令,这个才是 CPU 能够真正认识的东西。

下面说明 a = 1 + 2 在 32 位 CPU 的执行过程。

程序编译过程中,编译器通过分析代码,发现 1 和 2 是数据,于是程序运行时,内存会有个专门的区域来存放这些数据,这个区域就是数据段。如下图,数据 1 和 2 的区域位置:

  • 数据 1 被存放到 0x200 位置;
  • 数据 2 被存放到 0x204 位置;

数据和指令是分开区域存放的,存放指令区域的地方称为正文段

编译器会把 a = 1 + 2 翻译成 4 条指令,存放到正文段中。如图,这 4 条指令被存放到了 0x100 ~ 0x10c 的区域中:

  • 0x100 的内容是 load 指令将 0x200 地址中的数据 1 装入到寄存器 R0;
  • 0x104 的内容是 load 指令将 0x204 地址中的数据 2 装入到寄存器 R1;
  • 0x108 的内容是 add 指令将寄存器 R0 和 R1 的数据相加,并把结果存放到寄存器 R2;
  • 0x10c 的内容是 store 指令将寄存器 R2 中的数据存回数据段中的 0x208 地址中,这个地址也就是变量 a 内存中的地址;

编译完成后,具体执行程序的时候,程序计数器会被设置为 0x100 地址,然后依次执行这 4 条指令。

上面的例子中,由于是在 32 位 CPU 执行的,因此一条指令是占 32 位大小,所以你会发现每条指令间隔 4 个字节。

而数据的大小是根据你在程序中指定的变量类型,比如 int 类型的数据则占 4 个字节,char 类型的数据则占 1 个字节。

1.2 指令

指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来知道指令的内容。

不同的 CPU 有不同的指令集,也就是对应着不同的汇编语言和不同的机器码。

编译器在编译程序的时候,会构造指令,这个过程叫做指令的编码。CPU 执行程序的时候,就会解析指令,这个过程叫作指令的解码。

现代大多数 CPU 都使用来流水线的方式来执行指令,所谓的流水线就是把一个任务拆分成多个小任务,于是一条指令通常分为 4 个阶段,称为 4 级流水线,如下图:

四个阶段的具体含义:

  • CPU 通过程序计数器读取对应内存地址的指令,这个部分称为 Fetch(取得指令)
  • CPU 对指令进行解码,这个部分称为 Decode(指令译码)
  • CPU 执行指令,这个部分称为 Execution(执行指令)
  • CPU 将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为 Store(数据回写)

上面这 4 个阶段,我们称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始。

不同的阶段其实是由计算机中的不同组件完成的:

事实上,不同的阶段其实是由计算机中的不同组件完成的:

  • 取指令的阶段,我们的指令是存放在存储器里的,实际上,通过程序计数器和指令寄存器取出指令的过程,是由控制器操作的;
  • 指令的译码过程,也是由控制器进行的;
  • 指令执行的过程,无论是进行算术操作、逻辑操作,还是进行数据传输、条件分支操作,都是由算术逻辑单元操作的,也就是由运算器处理的。但是如果是一个简单的无条件地址跳转,则是直接在控制器里面完成的,不需要用到运算器。

1.3 指令的类型

根据功能划分,可分为5大类指令:

  • 数据传输类型,比如 store/load 是寄存器与内存间数据传输的指令,mov 是将一个内存地址的数据移动到另一个内存地址的指令;
  • 运算类型的指令,,比如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据;
  • 跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的 if-else、switch-case、函数调用等。
  • 信号类型的指令,比如发生中断的指令 trap;
  • 闲置类型的指令,比如指令 nop,执行后 CPU 会空转一个周期;

1.4 指令的执行速度

CPU 的硬件参数都会有 GHz 这个参数,指的是时钟频率,代表着 1 秒会产生 多少 G 次的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期

对于 CPU 来说,在一个时钟周期内,CPU 仅能完成一个最基本的动作,时钟频率越高,时钟周期就越短,工作速度也就越快。

一个时钟周期一定能执行完一条指令吗?

不一定,大多数指令不能在一个时钟周期完成,通常需要若干个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着一条 CPU 指令,但是乘法需要的时钟周期就要比加法多。

CPU执行时间

对于程序的 CPU 执行时间,我们可以拆解成 CPU 时钟周期数(CPU Cycles)时钟周期时间(Clock Cycle Time)的乘积:

程序的 CPU 执行时间 = CPU 时钟周期数 X 时钟周期时间
2.4 GHz 主频的CPU,时钟周期时间就是 1/2.4G。

对于 CPU 时钟周期数我们可以进一步拆解成:「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)」,于是程序的 CPU 执行时间的公式可变成如下:

程序的 CPU 执行时间 = 指令数 X CPI X 时钟周期时间

因此,要想程序跑的更快,优化这三者即可:

  • 指令数,表示执行程序所需要多少条指令,以及哪些指令。这个层面是基本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译出来的计算机指令会有各种不同的表示方式。
  • 每条指令的平均时钟周期数 CPI,表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU 时钟周期数尽可能的少;
  • 时钟周期时间,表示计算机主频,取决于计算机硬件。有的 CPU 支持超频技术,打开了超频意味着把 CPU 内部的时钟给调快了,于是 CPU 工作速度就变快了,但是也是有代价的,CPU 跑的越快,散热的压力就会越大,CPU 会很容易奔溃。