关于java:CPU-执行程序的秘密藏在了这-15-张图里

31次阅读

共计 8121 个字符,预计需要花费 21 分钟才能阅读完成。

前言

代码写了那么多,你晓得 a = 1 + 2 这条代码是怎么被 CPU 执行的吗?

软件用了那么多,你晓得软件的 32 位和 64 位之间的区别吗?再来 32 位的操作系统能够运行在 64 位的电脑上吗?64 位的操作系统能够运行在 32 位的电脑上吗?如果不行,起因是什么?

CPU 看了那么多,咱们都晓得 CPU 通常分为 32 位和 64 位,你晓得 64 位相比 32 位 CPU 的劣势在哪吗?64 位 CPU 的计算性能肯定比 32 位 CPU 高很多吗?

不晓得也不必慌乱,接下来就循序渐进的、一层一层的攻破这些问题。

image


注释

图灵机的工作形式

要想晓得程序执行的原理,咱们能够先从「图灵机」说起,图灵的根本思维是用机器来模仿人们用纸笔进行数学运算的过程,而且还定义了计算机由哪些局部组成,程序又是如何执行的。

图灵机长什么样子呢?你从下图能够看到图灵机的理论样子:

image

<figcaption > 图起源自:http://www.kristergustafsson.me/turing-machine/</figcaption>

图灵机的根本组成如下:

  • 有一条「纸带」,纸带由一个个间断的格子组成,每个格子能够写入字符,纸带就好比内存,而纸带上的格子的字符就好比内存中的数据或程序;
  • 有一个「读写头」,读写头能够读取纸带上任意格子的字符,也能够把字符写入到纸带的格子;
  • 读写头上有一些部件,比方存储单元、管制单元以及运算单元:1、存储单元用于存放数据;2、管制单元用于辨认字符是数据还是指令,以及控制程序的流程等;3、运算单元用于执行运算指令;

晓得了图灵机的组成后,咱们以简略数学运算的 1 + 2 作为例子,来看看它是怎么执行这行代码的。

  • 首先,用读写头把「1、2、+」这 3 个字符别离写入到纸带上的 3 个格子,而后读写头先停在 1 字符对应的格子上;

image

  • 接着,读写头读入 1 到存储设备中,这个存储设备称为图灵机的状态;

image

  • 而后读写头向右挪动一个格,用同样的形式把 2 读入到图灵机的状态,于是当初图灵机的状态中存储着两个间断的数字,1 和 2;

image

  • 读写头再往右挪动一个格,就会碰到 + 号,读写头读到 + 号后,将 + 号传输给「管制单元」,管制单元发现是一个 + 号而不是数字,所以没有存入到状态中,因为 + 号是运算符指令,作用是加和目前的状态,于是告诉「运算单元」工作。运算单元收到要加和状态中的值的告诉后,就会把状态中的 1 和 2 读入并计算,再将计算的后果 3 寄存到状态中;

image

  • 最初,运算单元将后果返回给管制单元,管制单元将后果传输给读写头,读写头向右挪动,把后果 3 写入到纸带的格子中;

image

通过下面的图灵机计算 1 + 2 的过程,能够发现图灵机次要性能就是读取纸带格子中的内容,而后交给管制单元辨认字符是数字还是运算符指令,如果是数字则存入到图灵机状态中,如果是运算符,则告诉运算符单元读取状态中的数值进行计算,计算结果最终返回给读写头,读写头把后果写入到纸带的格子中。

事实上,图灵机这个看起来很简略的工作形式,和咱们明天的计算机是根本一样的。接下来,咱们一起再看看当今计算机的组成以及工作形式。


冯诺依曼模型

在 1945 年冯诺依曼和其余计算机科学家们提出了计算机具体实现的报告,其遵循了图灵机的设计,而且还提出用电子元件结构计算机,并约定了用二进制进行计算和存储,还定义计算机根本构造为 5 个局部,别离是中央处理器(CPU)、内存、输出设施、输出设备、总线。

image

这 5 个局部也被称为冯诺依曼模型,接下来看看这 5 个局部的具体作用。

内存

咱们的程序和数据都是存储在内存,存储的区域是线性的。

数据存储的单位是一个二进制位(bit),即 0 或 1。最小的存储单位是字节(byte)**,1 字节等于 8 位。

内存的地址是从 0 开始编号的,而后自增排列,最初一个地址为内存总字节数 – 1,这种构造好似咱们程序里的数组,所以内存的读写任何一个数据的速度都是一样的。

中央处理器

中央处理器也就是咱们常说的 CPU,32 位和 64 位 CPU 最次要区别在于一次能计算多少字节数据:

  • 32 位 CPU 一次能够计算 4 个字节;
  • 64 位 CPU 一次能够计算 8 个字节;

这里的 32 位和 64 位,通常称为 CPU 的位宽。

之所以 CPU 要这样设计,是为了能计算更大的数值,如果是 8 位的 CPU,那么一次只能计算 1 个字节 0~255 范畴内的数值,这样就无奈一次实现计算 10000 * 500,于是为了能一次计算大数的运算,CPU 须要反对多个 byte 一起计算,所以 CPU 位宽越大,能够计算的数值就越大,比如说 32 位 CPU 能计算的最大整数是 4294967295。

CPU 外部还有一些组件,常见的有寄存器、管制单元和逻辑运算单元等。其中,管制单元负责管制 CPU 工作,逻辑运算单元负责计算,而寄存器能够分为多品种,每种寄存器的性能又不尽相同。

CPU 中的寄存器次要作用是存储计算时的数据,你可能好奇为什么有了内存还须要寄存器?起因很简略,因为内存离 CPU 太远了,而寄存器就在 CPU 里,还紧挨着管制单元和逻辑运算单元,天然计算时速度会很快。

常见的寄存器品种:

  • 通用寄存器,用来寄存须要进行运算的数据,比方须要进行加和运算的两个数据。
  • 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,留神不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令的地址。
  • 指令寄存器,用来存放程序计数器指向的指令,也就是指令自身,指令被执行实现之前,指令都存储在这里。

总线

总线是用于 CPU 和内存以及其余设施之间的通信,总线可分为 3 种:

  • 地址总线,用于指定 CPU 将要操作的内存地址;
  • 数据总线,用于读写内存的数据;
  • 管制总线,用于发送和接管信号,比方中断、设施复位等信号,CPU 收到信号后天然进行响应,这时也须要管制总线;

当 CPU 要读写内存数据的时候,个别须要通过两个总线:

  • 首先要通过「地址总线」来指定内存的地址;
  • 再通过「数据总线」来传输数据;

输出、输出设备

输出设施向计算机输出数据,计算机通过计算后,把数据输入给输出设备。期间,如果输出设施是键盘,按下按键时是须要和 CPU 进行交互的,这时就须要用到管制总线了。


线路位宽与 CPU 位宽

数据是如何通过地址总线传输的呢?其实是通过操作电压,低电压示意 0,低压电压则示意 1。

如果结构了高下高这样的信号,其实就是 101 二进制数据,十进制则示意 5,如果只有一条线路,就意味着每次只能传递 1 bit 的数据,即 0 或 1,那么传输 101 这个数据,就须要 3 次能力传输实现,这样的效率非常低。

这样一位一位传输的形式,称为串行,下一个 bit 必须期待上一个 bit 传输实现能力进行传输。当然,想一次多传一些数据,减少线路即可,这时数据就能够并行传输。

为了防止低效率的串行传输的形式,线路的位宽最好一次就能拜访到所有的内存地址。CPU 要想操作的内存地址就须要地址总线,如果地址总线只有 1 条,那每次只能示意「0 或 1」这两种状况,所以 CPU 一次只能操作 2 个内存地址;如果想要 CPU 操作 4G 的内存,那么就须要 32 条地址总线,因为 2 ^ 32 = 4G。

晓得了线路位宽的意义后,咱们再来看看 CPU 位宽。

CPU 的位宽最好不要小于线路位宽,比方 32 位 CPU 管制 40 位宽的地址总线和数据总线的话,工作起来就会非常复杂且麻烦,所以 32 位的 CPU 最好和 32 位宽的线路搭配,因为 32 位 CPU 一次最多只能操作 32 位宽的地址总线和数据总线。

如果用 32 位 CPU 去加和两个 64 位大小的数字,就须要把这 2 个 64 位的数字分成 2 个低位 32 位数字和 2 个高位 32 位数字来计算,先加个两个低位的 32 位数字,算出进位,而后加和两个高位的 32 位数字,最初再加上进位,就能算出后果了,能够发现 32 位 CPU 并不能一次性计算出加和两个 64 位数字的后果。

对于 64 位 CPU 就能够一次性算出加和两个 64 位数字的后果,因为 64 位 CPU 能够一次读入 64 位的数字,并且 64 位 CPU 外部的逻辑运算单元也反对 64 位数字的计算。

然而并不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少利用须要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的状况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的状况下,64 位的劣势能力体现进去。

另外,32 位 CPU 最大只能操作 4GB 内存,就算你装了 8 GB 内存条,也没用。而 64 位 CPU 寻址范畴则很大,实践最大的寻址空间为 2^64。


程序执行的根本过程

在后面,咱们晓得了程序在图灵机的执行过程,接下来咱们来看看程序在冯诺依曼模型上是怎么执行的。

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

image

那 CPU 执行程序的过程如下:

  • 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,而后 CPU 的「管制单元」操作「地址总线」指定须要拜访的内存地址,接着告诉内存设施筹备数据,数据筹备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
  • 第二步,CPU 剖析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「管制单元」执行;
  • 第三步,CPU 执行完指令后,「程序计数器」的值自增,示意指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比方 32 位的 CPU,指令是 4 个字节,须要 4 个内存地址寄存,因而「程序计数器」的值会自增 4;

简略总结一下就是,一个程序执行的时候,CPU 会依据程序计数器里的内存地址,从内存外面把须要执行的指令读取到指令寄存器外面执行,而后依据指令长度自增,开始程序读取下一条指令。

CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会一直循环,直到程序执行完结,这个一直循环的过程被称为 CPU 的指令周期。


a = 1 + 2 执行具体过程

晓得了根本的程序执行过程后,接下来用 a = 1 + 2 的作为例子,进一步剖析该程序在冯诺伊曼模型的执行过程。

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

针对汇编代码,咱们还须要用汇编器翻译成机器码,这些机器码由 0 和 1 组成的机器语言,这一条条机器码,就是一条条的计算机指令,这个才是 CPU 可能真正意识的货色。

上面来看看 a = 1 + 2 在 32 位 CPU 的执行过程。

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

  • 数据 1 被寄存到 0x100 地位;
  • 数据 2 被寄存到 0x104 地位;

留神,数据和指令是离开区域寄存的,寄存指令区域的中央称为「注释段」。

image

编译器会把 a = 1 + 2 翻译成 4 条指令,寄存到注释段中。如图,这 4 条指令被寄存到了 0x200 ~ 0x20c 的区域中:

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

编译实现后,具体执行程序的时候,程序计数器会被设置为 0x200 地址,而后顺次执行这 4 条指令。

下面的例子中,因为是在 32 位 CPU 执行的,因而一条指令是占 32 位大小,所以你会发现每条指令距离 4 个字节。

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

指令

下面的例子中,图中指令的内容我写的是繁难的汇编代码,目标是为了不便了解指令的具体内容,事实上指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU 通过解析机器码来晓得指令的内容。

不同的 CPU 有不同的指令集,也就是对应着不同的汇编语言和不同的机器码,接下来选用最简略的 MIPS 指集,来看看机器码是如何生成的,这样也能明确二进制的机器码的具体含意。

MIPS 的指令是一个 32 位的整数,高 6 位代表着操作码,示意这条指令是一条什么样的指令,剩下的 26 位不同指令类型所示意的内容也就不雷同,次要有三种类型 R、I 和 J。

image

一起具体看看这三种类型的含意:

  • R 指令,用在算术和逻辑操作,外面由读取和写入数据的寄存器地址。如果是逻辑位移操作,前面还有位移操作的「位移量」,而最初的「性能码」则是再后面的操作码不够的时候,扩大操作码来示意对应的具体指令的;
  • I 指令,用在数据传输、条件分支等。这个类型的指令,就没有了位移量和操作码,也没有了第三个寄存器,而是把这三局部间接合并成了一个地址值或一个常数;
  • J 指令,用在跳转,高 6 位之外的 26 位都是一个跳转后的地址;

接下来,咱们把后面例子的这条指令:「add 指令将寄存器 R0 和 R1 的数据相加,并把后果放入到 R3」,翻译成机器码。

image

加和运算 add 指令是属于 R 指令类型:

  • add 对应的 MIPS 指令里操作码是 000000,以及最开端的性能码是 100000,这些数值都是固定的,查一下 MIPS 指令集的手册就能晓得的;
  • rs 代表第一个寄存器 R0 的编号,即 00000;
  • rt 代表第二个寄存器 R1 的编号,即 00001;
  • rd 代表指标的长期寄存器 R2 的编号,即 00010;
  • 因为不是位移操作,所以位移量是 00000

把下面这些数字拼在一起就是一条 32 位的 MIPS 加法指令了,那么用 16 进制示意的机器码则是 0x00011020。

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

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

image

四个阶段的具体含意:

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

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

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

image

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

指令的类型

指令从性能角度划分,能够分为 5 大类:

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

指令的执行速度

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

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

一个时钟周期肯定能执行完一条指令吗?答案是不肯定的,大多数指令不能在一个时钟周期实现,通常须要若干个时钟周期。不同的指令须要的时钟周期是不同的,加法和乘法都对应着一条 CPU 指令,然而乘法须要的时钟周期就要比加法多。

如何让程序跑的更快?

程序执行的时候,消耗的 CPU 工夫少就阐明程序是快的,对于程序的 CPU 执行工夫,咱们能够拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积 **。

image

时钟周期时间就是咱们后面提及的 CPU 主频,主频越高阐明 CPU 的工作速度就越快,比方我手头上的电脑的 CPU 是 2.4 GHz 四核 Intel Core i5,这里的 2.4 GHz 就是电脑的主频,时钟周期时间就是 1/2.4G。

要想 CPU 跑的更快,天然缩短时钟周期时间,也就是晋升 CPU 主频,然而今非彼日,摩尔定律早已生效,当今的 CPU 主频曾经很难再做到翻倍的成果了。

另外,换一个更好的 CPU,这个也是咱们软件工程师控制不了的事件,咱们应该把眼光放到另外一个乘法因子 —— CPU 时钟周期数,如果能缩小程序所需的 CPU 时钟周期数量,一样也是能晋升程序的性能的。

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

image

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

  • 指令数,示意执行程序所须要多少条指令,以及哪些指令。这个层面是根本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译进去的计算机指令会有各种不同的示意形式。
  • 每条指令的均匀时钟周期数 CPI,示意一条指令须要多少个时钟周期数,古代大多数 CPU 通过流水线技术(Pipline),让一条指令须要的 CPU 时钟周期数尽可能的少;
  • 时钟周期时间,示意计算机主频,取决于计算机硬件。有的 CPU 反对超频技术,关上了超频意味着把 CPU 外部的时钟给调快了,于是 CPU 工作速度就变快了,然而也是有代价的,CPU 跑的越快,散热的压力就会越大,CPU 会很容易奔溃。

很多厂商为了跑分而跑分,根本都是在这三个方面动手的哦,特地是超频这一块。


总结

最初咱们再来答复结尾的问题。

64 位相比 32 位 CPU 的劣势在哪吗?64 位 CPU 的计算性能肯定比 32 位 CPU 高很多吗?

64 位相比 32 位 CPU 的劣势次要体现在两个方面:

  • 64 位 CPU 能够一次计算超过 32 位的数字,而 32 位 CPU 如果要计算超过 32 位的数字,要分多步骤进行计算,效率就没那么高,然而大部分应用程序很少会计算那么大的数字,所以只有运算大数字的时候,64 位 CPU 的劣势能力体现进去,否则和 32 位 CPU 的计算性能相差不大。
  • 64 位 CPU 能够寻址更大的内存空间,32 位 CPU 最大的寻址地址是 4G,即便你加了 8G 大小的内存,也还是只能寻址到 4G,而 64 位 CPU 最大寻址地址是 2^64,远超于 32 位 CPU 最大寻址地址的 2^32。

你晓得软件的 32 位和 64 位之间的区别吗?再来 32 位的操作系统能够运行在 64 位的电脑上吗?64 位的操作系统能够运行在 32 位的电脑上吗?如果不行,起因是什么?

64 位和 32 位软件,实际上代表指令是 64 位还是 32 位的:

  • 如果 32 位指令在 64 位机器上执行,须要一套兼容机制,就能够做到兼容运行了。然而如果 64 位指令在 32 位机器上执行,就比拟艰难了,因为 32 位的寄存器存不下 64 位的指令;
  • 操作系统其实也是一种程序,咱们也会看到操作系统会分成 32 位操作系统、64 位操作系统,其代表意义就是操作系统中程序的指令是多少位,比方 64 位操作系统,指令也就是 64 位,因而不能装在 32 位机器上。

总之,硬件的 64 位和 32 位指的是 CPU 的位宽,软件的 64 位和 32 位指的是指令的位宽。

看完三件事

❤️如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:
点赞,转发,有你们的『点赞和评论』,才是我发明的能源。
关注公众号『Java 斗帝』,不定期分享原创常识。
同时能够期待后续文章 ing????

正文完
 0