1.冯诺依曼模型
内存:我们的程序和数据都是存储在内存,存储的区域是线性的。数据存储的单位是一个二进制位,即0或1。最小的存储单位是字节,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进行交互的,这时就需要用到控制总线了。
2.线路位宽与CPU位宽
数据是如何通过线路传输的呢?其实是通过操作电压,低电压表示0,高电压则表示1.如果构造了高低高这样的信号,其实就是101二进制数据,十进制则表示5,如果只有一条线路,就意味着每次只能传递1bit的数据,即0或1,那么传输101这个数据,就需要3次才能传输完成,这样的效率非常低。这样一位一位传输的方式,称为串行,下一个bit必须等待上一个bit传输完成才能进行传输。当然,想一次多传一些数据,增加线路即可,这时数据就可以并行传输。为了避免低效率的串行传输方式,线路的位宽最好一次就能访问到所有的内存地址。CPU要想操作的内存地址就需要地址总线,如果地址总线只有一条,那每次只能表示【0或1】这两种情况,所以CPU一次只能操作2个内存地址,如果想要CPU操作4G的内存,那么就需要32条地址总线,因为2^32=4G。
CPU的位宽最好不要小于线路位宽,比如32位CPU控制40位宽的地址总线和数据总线的话,工作起来就会非常复杂且麻烦,所以32位的CPU最好和32位宽的线路搭配,因为32位CPU一次最多只能操作32位宽的地址总线和数据总线。
如果用32位CPU去加和两个64位大小的数字,就需要把这2个64位的数字分成两个低位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内存,就算你装了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执行具体过程
CPU是不认识a=1+2这个字符串,这些字符串只是方便我们程序员认识,要想这段程序能跑起来,还需要把整个程序翻译成汇编语言的程序,这个过程称为编译成汇编代码。针对汇编代码,我们还需要用汇编语言翻译成机器码,这些机器码由0和1组成的机器语言,这一条条机器码,就是一条条的计算机指令,这个才是CPU能够真正认识的东西。
程序编译过程中,编译器通过分析代码,发现1和2是数据,于是程序运行时,内存会有个专门的区域来存放这些数据,这个区域就是数据段。
·数据1被存放到0x100位置;
·数据2被存放到0x104位置;
注意,数据和指令是分开区域存放的,存放指令区域的地方称为正文段。
编译器会把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类型的数据则占一个字节。
指令:
上面的例子中,图中指令的内容我写的是简易的汇编代码,目的是为了方便理解指令的具体内容,事实上指令的内容是一串二进制数字的机器码,每条指令都有对应的机器码,CPU通过解析机器码来知道指令的内容。不同的CPU有不同的指令集,也就是对应着不同的汇编语言和不同的机器码,接下来选用最简单的MIPS指集,来看看机器码是如何生成的,这样也能明白二进制的机器码的具体含义。MIPS的指令是一个32位的整数,高6位代表着操作码,表示这条指令是一条什么样的指令,剩下的26位不同指令类型所表示的内容也就不相同,主要有三种类型R,I和J。
三种类型的含义:
·R指令,用在算数和逻辑操作,里面由读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有位移的位移量,而最后的功能码则是在前面的操作码不够的时候,扩展操作码来表示对应的具体指令的;
·I指令,用在数据传输,条件分支等。这个类型的指令,就没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或一个常数;
·J指令,用在跳转,高6位之外的26位都是一个跳转后的地址;
接下来,我们把前面例子的这条指令:add指令寄存器R0和R1的数据相加,并把结果放入R3,翻译成机器码。
加和运算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级流水线:
四个阶段的含义:
·CPU通过程序计数器读取对应的内存地址的指令,这个部分称为Fetch(取得指令)
·CPU对指令进行解码,这个部分称为Decode(指令译码)
·CPU执行指令,这个部分称为Execution(执行指令)
·CPU将计算结果存回寄存器或者将寄存器的值存入内存,这个部分称为Store(数据回写)
上面这四个阶段,我们称之为指令周期,CPU的工作就是一个周期接着一个周期,周而复始。事实上,不同的阶段是由计算机中的不同组件完成的:
·取指令的阶段,我们的指令是存放在存储器里的,实际上,通过程序计数器和指令寄存器取出指令的过程,是由控制器操作的;
·指令的译码过程,也是由控制器进行的;
·指令执行的过程,无论是进行算数操作,逻辑操作,还是进行数据传输,条件分支操作,都是由算数逻辑单元操作的,也就是由运算器处理的。但是如果是一个简单的无条件地址跳转,则是直接在控制器里面完成的,不需要用到运算器。
指令的类型:
·数据传输类型的指令,比如store/load是寄存器与内存之间数据传输的指令,mov是将一个内存地址的数据移动到另一个内存地址的指令;
·运算类型的指令,比如加减乘除,位运算,比较大小等等,它们最多只能处理两个寄存器中的数据;
·跳转类型的指令,通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的if-else,switch-case,函数调用等。
·信号类型的指令,比如发生中断的指令trap;
·闲置类型的指令,比如指令nop,执行后cpu会空转一个周期;
指令的执行速度:
CPU的硬件参数都会有GHz这个参数,比如1GHz的CPU,指的是时钟频率是1G,代表着1秒会产生1G次数的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期。
对于CPU来说,在一个时钟周期内,CPU仅能完成一个最基本的动作,时钟频率越高,时钟周期越短,工作速度也就越快。
一个周期一定可以执行完一条指令嘛?大多数是不一定的,大多数指令不能再一个时钟周期完成,通常需要若干个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着一条CPU指令,但是乘法需要的时钟周期就要比加法多。
5.如何让程序跑的更快?
程序执行的时候,耗费的CPU时间少就说明程序是快的,对于程序的CPU执行时间,我们可以拆解成CPU时钟周期数和时钟周期时间的乘积。
时钟周期时间就是我们前面提及的CPU主频,主频越高说明CPU的工作速度就越快,比如电脑的CPU是2.4GHz四核Intel Core i5,这里的2.4GHz就是电脑的主频,时钟周期时间就是1/2.4G。
要想CPU跑的更快自然缩短时钟周期时间,也就是提升CPU主频,但是今非昔比,摩尔定律早已失效,当今的CPU主频已经很难做到翻倍的效果了。另外,换一个更好的CPU是软件工程师无法控制的事情,我们应该把目光放到另一个乘法因子----CPU时钟周期数,如果能减少所需的CPU时钟周期数量,一样也是能提升程序的性能的。
对于CPU时钟周期数我们可以进一步拆解成:(指令数x每条指令的平均时钟周期数),于是程序的CPU执行时间的公式改变:
所以要想程序跑的快,优化这三个因子:
· 指令数,表示执行程序所需要多少条指令,以及那些指令。这个层面是基本靠编译器来优化,毕竟同样的代码,在不同的编译器,编译出来的计算机指令会有各种不同的表示方式。
·每条指令的平均时钟周期数,表示一条指令需要多少个时钟周期数,现代大多数CPU通过流水线技术,让一条指令需要的CPU时钟周期数尽可能的少;
·时钟周期时间,表示计算机主频,取决于计算机硬件。有的CPU支持超频技术,打开超频意味着把CPU内部的时钟给调快了,于是CPU工作速度就变快了,但是也是有代价的,CPU跑的越快,散热的压力就会越大,CPU会很容易崩溃。
本文参考:https://mp.weixin.qq.com/s/TxFzIgNLettiEO4JKWgQpQ