前言:a=1+2这条代码是如何被CPU执行的呢?32位和64操作系统有什么区别呢?
注意:各位大佬,如果有哪里写的不清楚,请在邮件pigmn@sina.com友善的交流。
1.冯诺依曼模型
1945年冯诺依曼和其他计算机科学家提出了计算机具体实现的报告,遵循了图灵机的设计,提出用电子元件构造计算机,并约定用二进制进行计算和存储。
计算机的基本结构分成五个部分,分别是运算器、控制器、存储器、输入设备和输出设备,这5个部分也被称为冯诺依曼模型,如图1所示:
图1 冯诺依曼模型
运算器、控制器是在中央处理器里的,存储器就我们常见的内存,输入输出设备则是计算机外接的设备,比如键盘就是输入设备,显示器就是输出设备。
存储单元和输入输出设备要与中央处理器交换数据的话,需要使用总线。它们的具体关系如图2所示:
图2 五大计算机结构的关系示意图
内存
我们的程序和数据都是存储在内存中,存取的区域是线性的。
数据存储的单位是一个二进制位,即0或1。最小的存储单位是字节,1字节等于8位。
内存的地址是从0开始编号的,然后自增排序,最后一个地址为内存总字节数-1,这种结构好似我们程序里的数组,所以内存的读写任何一个数据的速度都是一样的。
中央处理器
中央处理器也就是我们常说的CPU,32位和64位CPU最主要区别在于一次能计算多少字节数据:
- 32位CPU一次可以计算4个字节;
- 64位CPU一次可以计算8个字节;
这里的32位和64位,通常称为CPU的位宽。
CPU内部还有一些组件,常见的有寄存器、控制单元和逻辑运算单元等。其中,控制单元负责控制CPU工作,逻辑运算单元负责计算,而寄存器可以分为多种类,每种寄存器的功能又不尽相同。
CPU中的寄存器主要作用是存储计算时的数据,因为内存里CPU太远了,而寄存器在CPU内部,还紧挨着控制单元和逻辑运算单元,自然计算时速度会很快。
常见的寄存器种类:
- 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
- 程序寄存器,用来存储CPU要执行下一条指令【所在的内存地址】。
- 指令寄存器,用来存放程序计数器指向的指令,也就是指令本身,指令被执行完成之前,指令都存放在这里。
总线
总线是用于CPU和内存以及其他设备之间的通信,总线可以分为3种:
- 地址总线,用于指定CPU将要操作的内存地址;
- 数据总线,用于读写内存的数据;
- 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU收到信号后自然进行响应,这时也需要控制总线;
当CPU要读写内存数据的时候,一般需要通过两个总线:
首先要通过地址总线来指定内存的地址,再通过数据总线来传输数据。
输入、输出设备
输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和CPU进行交互的,这时就需要用到控制总线了。
2.线路位宽与CPU位宽
数据是如何通过线路传输的呢?其实是通过操作电压,低电压表示0,高电压则表示1.
如果构造了高低高这样的信号,其实就是101二进制数据,十进制表示5,如果只有一条线路,就意味着每次只能传递1bit的数据,即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个低位32位数字和2个高位32位数字来计算,先加两个低位的32位数字,算出进位,然后加和两个高位的32位数字,最后再加上进位,就能算出结果了,可以发现32位CPU并不能一次性算出加和两个64位数字的结果。
对于64位CPU就可以一次性算出加和两个64位数字的结果,因为64位CPU可以一次读入64位数字,并且64位CPU内部的逻辑结构运算单元也支持64位数字的计算。
但是并不代表32位数字的情况下,32位和64位CPU之间没什么区别,只有当计算超过32位数字的情况下,64位的优势才能体现出来。
另外,32位CPU最大只能操作4GB内存,就算你装了8GB内存条,也没用。而64位CPU寻址范围则很大,理论最大的寻址空间为2^64.
3.程序执行的基本过程
接下来我们来看看程序在冯诺依曼模型上是怎么执行的。程序实际上是一条条指令,所以程序的运行过程就是把每一条指令一步一步的执行起来,负责执行指令的就是CPU了。
那CPU执行程序的过程如下:
- 第一步,CPU读取程序计数器的值,这个值是指令的内存地址,然后CPU的控制单元操作地址总线指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过数据总线将指令数据传给CPU,CPU收到内存传来的数据后,将这个指令数据存入到指令寄存器。
- 第二步,CPU分析指令寄存器中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给逻辑运算单元运算;如果是存储类型的指令,则交给控制单元执行;
- 第三步,CPU执行完指令后,程序计数器的值自增,表示指向吓一跳指令。这个自增的大小,由CPU的位宽决定,比如32位的CPU,指令是4个字节,需要4个内存地址存放,因此程序计数器的值会自增4;
简单总结一下就是,一个程序执行的时候,CPU会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。
CPU从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,这个不断循环的过程被称为CPU的指令周期。
4. 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位置;
注意,数据和指令是分开存放的,存放指令区域的地方称为正文段。
图1 指令和数据存放分布情况
编译器会把a=1+2翻译成4条指令,存放到正文段中。如图,这4条指令被存放到了0x200~0x20c的区域中。
编译完成后,具体执行程序的时候,程序计数器会被设置为0x200地址,然后依次执行这4条指令。
上面的例子中,由于是在32位CPU执行的,因此一条指令时占32位大小,所以你会发现每条指令间隔4个字节。
而数据的大小是根据你在程序中指定的变量类型,比如int类型的数据则占4个字节,char类型的数据则占1个字节。
5.指令
上面的例子中,图中指令的内容是简易的汇编代码,目的是为了方便理解指令的具体内容,事实上指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU通过解析来知道指令的内容。
不同的cpu有不同的指令集,也就是对应着不同的汇编语言和不同的机器码,接下来选用最简单的MIPS指令集,来看看机器码是如何生成的,这样也能明白二进制的机器的具体含义。
MIPS的指令是一个32位的整数,高6位代表着操作码,表示这条指令是一条什么样的指令,剩下的26位不同指令类型表示的内容也就不相同,主要有三种类型 R/I/J。
图2 MIPS指令类型
- R指令:用在算术和逻辑操作,里面有读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有唯一操作的位移量,而最后的功能码则是在前面的操作码不够的时候,扩展操作码来表示对应的具体指令的;
- I指令,用在数据传输、条件分支等。这个类型的指令,就没有了位移量和功能吗,也没有了第三个寄存器,而是这三部分直接合并成了一个地址值或一个常数;
- J指令,用在跳转,高6位之外的26位都是一个跳转后的地址;
编译器在编译过程中,会构造指令,这个过程叫做指令的编码。CPU执行程序的时候,就会解析指令,这个过程叫做指令的解码。
现代大多数CPU都是用流水线的方式来执行指令,所谓的流水线就是把一个任我拆分成多个小任务,于是一条指令通常分为4个阶段,如下图:
图3 CPU执行指令的过程
四个阶段的含义:
- CPU通过程序计数器读取对应内存地址的指令,这个部分称为Fetch;
- CPU对指令进行解码,这个部分称为Decode;
- CPU执行指令,这个部分称为Execution;
- CPU将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为Store;
上面这4个阶段,我们称为指令周期,CPU的工作就是一个周期接着一个周期,周而复始。
6.指令的类型
指令从功能角度划分,可以分外香5大类:
- 数据传输类型的指令,比如store、load是寄存器与内存间数据传输的指令,mov是讲一个内存地址的数据移动到另一个内存地址的指令;
- 运算类型的指令比如加减乘除、位运算、比较大小等等,它们最多只能处理两个寄存器中的数据。
- 跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的if-else/switch-case/函数调用等。
- 信号类型的指令,比如发生中断的指令trap;
- 闲置类型的指令,比如指令nop,执行后CPU会空转一个周期;
7.指令的执行速度
CPU的硬件参数都会有GHz这个参数,比如一个1GHz的CPU,指的是时钟频率是1G,代表着1秒会产生1G次数的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期。
对于CPU来说,在一个时钟周期内,CPU仅能完成一个最基本的动作,时钟频率越高,时钟周期就越短,工作速度也就越快。
一个时钟周期一定能执行完一条指令么?答案是不一定的,大多数指令不能在一个时钟周期完成,通常需要若干个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着一条CPU指令,但是乘法需要的时钟周期就要比加法多。
如何让程序跑的更快?
程序执行的时候,耗费的CPU时间少就说明程序是快的,对于程序的CPU执行时间,我们可以拆解成CPU时钟周期数和时钟周期时间的乘积。
时钟周期时间就是我们前面提及的CPU主频,主频越高说明CPU的工作速度就越快,比如2.4GHz主频的电脑,时钟周期时间就是1/2.4G。要想CPU跑的更快,自然缩短时钟周期时间,也就是提升CPU主频,但是当今的CPU主频已经很难提升了。
我们应该把目光放到另外一个乘法因子——CPU时钟周期数。
对于CPU时钟周期数我们可以进一步拆解成:指令数*每条指令的平均时钟周期数(Cycles Per instructions, CPI)。所以有:
程序的CPU执行时间=指令数*CPI*时钟周期时间
因此,要想程序跑的更快,优化这三者即可:
- 指令数,表示执行程序所需要多少条指令,以及那些指令。这个层面是基本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译出来的计算机指令会有各种不同的表示方式。
- 每条指令的平均时钟周期数CPI,让一条指令需要的CPU时钟周期数尽可能的少。
- 时钟周期时间,表示计算机主频,取决于计算机硬件。有的CPU支持超频技术,打开了超频意味着把CPU内部的时钟给调快了,于是CPU工作速度就变快了,但是也是有代价的,CPU跑的越快,散热的压力就会越大,CPU会容易崩溃。
8.总结
64位相比32万箱CPU的优势主要体现在两个方面:
- 64位CPU可以一次计算超过32位的数字,而32位CPU若果计算超过32位的数字,要分多步骤进行计算,效率就没那么高,但是大部分应用很少会计算那么大的数字,所以只有运算大数字的时候,64位CPU的优势才能体现出来,否则和32位CPU的计算性能相差不大。
- 64位CPU可以寻址更大的内存空间,32位CPU最大的寻址地址是4G,即使你加了8G大小的内存,也还是只能寻址到4G,而64位CPU最大寻址地址是2^64.