写给前端的 计算机组成原理 "十全大补文"

前端开发人员中,有相当大比例的同学不是科班出来的,所以对于基本的科班必修课,例如:计算机组成原理、操作系统、计算机网络、数据结构和算法等知识接触不多。

当你越深入学习,越会发现这些知识的重要性。

比如大家都知道js里面0.1 + 0.2 是不等于0.3的,为什么呢?这就牵扯到计算机组成原理中浮点数的表示方法,以及浮点数的加减运算(正文会有白话版解答)。

又例如从键盘输入a+b这个指令,如何通过cpu的调度输出到屏幕上呢?这就涉及到冯诺依曼体系,如果你是编程人员,都不清楚数据从键盘到屏幕的基本流向,那是时候看看这篇'十全大补文'了

本文是一篇计算机组成原理最基本的入门文章,我觉得前端没有必要那么深入这个专题,掌握基本的计算机组成原理的常识即可。

1、计算机的工作原理

首先,计算机最基本的5大组成部分如下图,分别为:输入设备(比如键盘), 存储器(比如内存), 运算器(cpu), 控制器(cpu), 输出设备(显示器)。

bbc073f6c9d61a443d7be97d402c3ba0.png

工作原理如下

1.1 控制器 ---> 控制输入设备 ----> 指令流向内存

当我们输入数据的时候,cpu里的控制器会让输入设备把这些指令存储到存储器(内存)上。

18b35e329c9de61fb39904b62ffb2cda.png

1.2 控制器分析指令 ---> 控制存储器 ---> 把数据送到运算器

控制器分析指令之后, 此时让存储器把数据发送到运算器里(控制器和运算器都在cpu里面)

这里需要注意,存储器既能存储数据,还能存储指令

29241be8004f7f7846e352cc294c8f05.png

1.3 控制器控制运算器做数据的运算 并且将运算结果返回存储器

984cb54523587e608b7434ce07c6dcc5.png

1.4 控制器控制存储器将结果返回给输出设备

4f2d49e0328990047397e8f06295cf57.png

从接下来,我们更近一步,看看计算机内部,CPU是怎么跟存储器交互的。

2、CPU及其工作过程

CPU中比较重要的两个部件是运算器和控制器,我们先来看看运算器的主要作用

2.1 运算器主要部件

5dde64de27c6d704c0a5af3d01f08014.png

如上图,运算器里最重要的部件是ALU,中文叫算术逻辑单元,用来进行算术和逻辑运算的。其它的MQ,ACC这些我们不用管了,是一些寄存器。

2.2 控制器主要部件

72f1760421493e37fbc331d7f6cffe80.png

控制器中最重要的部件是CU(控制单元),只要是分析指令,给出控制信号。

IR(指令寄存器),存放当前需要执行的指令

PC存放的指令的地址。

2.3 举例 - 取数指令执行过程

首先,是取指令的过程如下

d72bb56fec2f6f3e0c6e5d13671e6039.png
  • 第一步,PC,也就是存放指令地址的地方,我们要知道下一条指令是什么,就必须去存储器拿,CPU才知道接下来做什么。PC去了存储器的MAR拿要执行的指令地址,MAR(存储器里专门存指令地址的地方)

  • 第二步和第三步,MAR去存储体内拿到指令之后,将指令地址放入MDR(存储器里专门存数据的地方)

  • 第四步MDR里的数据返回到IR里面,IR是存放指令的地方,我们把刚才从存储体里拿的指令放在这里

然后,分析指令,执行指令的过程如下

916e8136a4b0af37964446c0d34940ad.png
  • 第五步, IR将指令放入CU中,来分析指令,比如说分析出是一个取数指令,接着就要执行指令了(这里取数指令,其实就是一个地址码,按着这个地址去存储体取数据)

  • 第六步,第七步 IR就会接着去找存储体里的MAR(存储地址的地方),MAR就根据取数指令里的地址吗去存储体里去数据

  • 第八步,取出的数据返回给MDR(存放数据的地方)

  • 第九步,MDR里的数据放到运算器的寄存器里,这里的取指令的过程结束了。

来个插曲,我们知道数据在内存里是二进制存着,也就是0和1, 0和1怎么用表示呢?我们拿其中一种存储0和1的方式来说明

电容是否有电荷,有电荷代表1,无电荷代表0

如下图

3c627d65bcff0639625a818ca5e0b612.png 04b6c5bbf87d4f5639bf3d7d53d8afbf.png

3、计算机编程语言

我们看看机器语言,怎么表示存放一个数的指令,例如下图

c79781a324ad692781da77b0fef33971.png

我们来看二进制代码 0000,0000,000000010000

  • 其中第一个0000,表示的是汇编语言里的LOAD,也就是加载,加载什么呢

  • 加载地址000000010000上的数据到第二个0000(寄存器的位置)。

接下来,我们看看如果是汇编语言怎么表示

39c5cd86dacce30b0bbbee3f4de88c38.png

LOAD A, 16意思是将存储体内的16号单元数据,放到寄存器地址A中 ADD C, A, B意思是将寄存器里的A,B数据相加,得到C 。STORE C, 17意思是将寄存器里的数据存到存储体17号单元内

最后,我们看看怎么用高级语言表示

11404f8a60ea9c6b41a94f8a430b85a6.png

高级语言是不是很简单,就一个a+b,你都不用去考虑寄存器,存储体这些事。

这部分的总结

高级语言一般有两种方式转换为机器语言

  • 一种是直接借助编译器,将高级语言转换为二进制代码,比如c,这样c运行起来就特别快,因为编译后是机器语言,直接就能在系统上跑,但问题是,编译的速度可能会比较慢。

  • 一种是解释性的,比如 js,是将代码翻译一行成机器语言(中间可能会先翻译为汇编代码或者字节码),解释一行,执行一行

需要注意的是,按照第一种将大量的高级代码翻译为机器语言,这其中就有很大的空间给编译器做代码优化,解释性语言就很难做这种优化,但是在v8引擎中,js还是要被优化的,在编译阶段(代码分编译和执行两个阶段)会对代码做一些优化,编译后立即执行的方式通常被称为 JIT (Just In Time) Comipler。

4、进制转换

接下来4.3这个小节会解释为什么0.1 + 0.2 等于0.3

4.1 二进制如何转化为十进制

例如2进制101.1如何转化为10进制。(有些同学觉得可以用parseInt('101.1', 2),这个是不行的,因为parseInt返回整数)

转化方法如下:

19632cdd64f8db94869786528a6c2ab7.png

上图的规则是什么呢?

二进制的每个数去乘以2的相应次方,注意小数点后是乘以它的负相应次方。再举一个例子你就明白了,二进制1101转为十进制

21ce832a98bc20957df0a825dbec1936.png

4.2 十进制整数转为二进制

JS里面可以用toString(2)这个方法来转换。如果要用通用的方法,例如:将十进制数(29)转换成二进制数, 算法如下:

  • 把给定的十进制数29除以2,商为14,所得的余数1是二进制数的最低位的数码

  • 再将14除以2,商为7,余数为0

  • 再将7除以2,商为3,余数为1,再将3除以2,商为1,余数为1

  • 再将1除以2,商为0,余数为1是二进制数的最高位的数码

c816939c072b89adbc9a8751c90c7ac9.png其结果为:11101

4.3 十进制小数转为二进制

方式是采用“乘2取整,顺序排列”法。具体做法是:

  • 用2乘十进制小数,可以得到积,将积的整数部分取出-

  • 再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出-

  • 如此进行,直到积中的小数部分为零,或者达到所要求的精度为止 我们具体举一个例子

如: 十进制 0.25 转为二进制

  • 0.25 * 2 = 0.5   取出整数部分:0

  • 0.5 * 2 = 1.0  取出整数部分1 即十进制0.25的二进制为 0.01 ( 第一次所得到为最高位,最后一次得到为最低位) 此时我们可以试试十进制0.1和0.2如何转为二进制

0.1(十进制) = 0.0001100110011001(二进制)
十进制数0.1转二进制计算过程:
0.1*2=0.2……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*2=1.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.8”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.6”接着计算。
0.6*2=1.2……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
0.2*2=0.4……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.4”接着计算。
0.4*2=0.8……0——整数部分为“0”。整数部分“0”清零后为“0”,用“0.2”接着计算。
0.8*2=1.6……1——整数部分为“1”。整数部分“1”清零后为“0”,用“0.2”接着计算。
……
……
所以,得到的整数依次是:“0”,“0”,“0”,“1”,“1”,“0”,“0”,“1”,“1”,“0”,“0”,“1”……。
由此,大家肯定能看出来,整数部分出现了无限循环。

接下来看0.2

0.2化二进制是
0.2*2=0.4,整数位为0
0.4*2=0.8,整数位为0
0.8*2=1.6,整数位为1,去掉整数位得0.6
0.6*2=1.2,整数位为1,去掉整数位得0.2
0.2*2=0.4,整数位为0
0.4*2=0.8.整数位为0
就这样推下去!小数*2整,一直下去就行
这个数整不断
0.0011001

所以0.1和0.2都无法完美转化为二进制,所以它们相加当然不是0.3了

5、定点数和浮点数

首先,什么是定点数呢?

5.1 定点数

75f073578be1e78bcd15a838d0e8da38.png

如上图,举例纯整数的二进制1011和-1011,如果是整数,符号位用0表示,如果是负数符号为用1表示

b4101d4d7ec32da82d51dcaf6f94c60b.png

同理,纯小数表示举例如下:

e95af4afc4c86e3f7212bb2abcee1730.png

那如果不是纯小数或者纯整数,该怎么表示呢?

比如10.1, 可以乘以一个比例因子,将10.1 ---> 101 比例因子是10, 或者10.1 ---> 0.101比例因子是100

定点数很简单,接下来我们介绍浮点数,再JS里面,数字都是用双精度的浮点数,所以学习浮点数对我们理解JS的数字有帮助。

5.2 浮点数

浮点数怎么表示呢?

7464d3367139551de5303e91eec9264b.png

上面是十进制的科学计数法,从中我们需要了解几个概念,一个是尾数,基数和阶码

  • 尾数必须是纯小数,所以上图中1.2345不满足尾数的格式,需要改成0.12345

  • 基数,在二进制里面是2

  • 阶码就是多少次方 所以浮点数的通用表示格式如下:

4dc868e9bc00aa48b7d1efed02d4f54c.png
  • S代表尾数

  • r代表基数

  • j代表阶码

这里需要注意的是,浮点数的加减运算,并不是像我们上面介绍的那样简单,会经过以下几个步骤完成

da73757e782c8b7eb02bf293c9bfc4e2.png

这些名词大家感兴趣的话,可以去网上查询,我们只要了解到浮点数加减运算很麻烦就行了,但如果你要做一个浮点数运算的库,你肯定是要完全掌握的。

6、局部性原理和catche(缓存)

先看下图

1aae1be76dc20f5d704599ec9e67f9f2.png

(说明一下,MDR和MAR虽然逻辑上属于主存,但是在电路实现的时候,MDR和MAR离CPU比较近) 上图是在执行一串代码,可以理解为js的for循环

const n = 1000;
const a = [1, 2, 3, 4, 5, 6, 7]
for(let i =0; i < n; i++) {
    a[i] = a[i] + 2
}

我们可以发现

  • 数组的数据有时候在内存是连续存储的

  • 如果我们要取数据,比如从内存取出a[0]的数据需要1000ns(ns是纳秒的意思),那么取出a[0]到a[7]就需要1000 * 8 = 8000 ns

  • 如果我们cpu发现这是取数组数据,那么我就把就近的数据块a[0]到a[7]全部存到缓存上多好,这样只需要取一次数据,消耗1000ns

cahce就是局部性原理的一个应用

6acf899b0bbbae266bd9fe28b39e8882.png
  • 空间局部性:在最近的未来要用到的信息(指令和数据),很可能与现在正在使用的信息在存储空间上是邻近的

  • 时间局部性:在最近的未来要用到的信息,很可能是现在正在使用的信息

9dbde1fb178f3b909738e2ab4a8a7007.png

可以看到cache一次性取了a[0]到a[9]存储体上的数据,只需要1000ns,因为Cache是高速存储器,跟cpu交互速度就比cpu跟主存交互速度快很多。

接下里,进入最后一节(略过对总线知识的学习),I/O设备的演变

7、I/O设备的演变

I/O是什么呢?

输入/输出(Input /Output ,简称I/O),指的是一切操作、程序或设备与计算机之间发生的数据传输过程。

比如文件读写操作,就是典型的I/O操作。接下来我们看一下I/O设备的演进过程

6dd69bff36ae04dbfa48befc4a4fc3d3.png

在早期的计算机里,cpu如何知道I/O设备已经完成任务呢?比如说怎么知道I/O设备已经读取完一个文件的数据呢?CPU会不断查询I/O设备是否已经准备好。这时,cpu就处于等待状态。也就是cpu工作的时候,I/O系统是不工作的,I/O系统工作,cpu是不工作。接着看第二阶段

1a804ec2b3a9133d092d8f44bf35e33c.png
  • 为了解决第一阶段CPU要等待I/O设备,串行的工作方式,所有I/O设备通过I/O总线来跟CPU打交道,一旦某个I/O设备完成任务,就会以中断请求的方式,通过I/O总线,告诉CPU,我已经准备好了。

  • 但是对于高速外设,它们完成任务的速度很快,所以会频繁中断CPU, 为了解决这个问题,高速外设跟主存之间用一条直接数据通路,DMA总线连接,CPU只需要安排开始高速外设做什么,剩下的就不用管了,这样就可以防止频繁中断CPU。最后来看一下第三阶段

dc648785740886aacc53c296aea0993c.png

第三阶段,CPU通过通道控制部件来管理I/O设备,CPU不需要帮它安排任务,只需要简单的发出启动和停止类似的命令,通道部件就会自动的安排相应的I/O设备工作

本文完结,希望大家点个赞,比心💗。

源码

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及 阿里面试前端精选资料 添加 下方小助手二维码或者扫描二维码 就可以进群。让我们一起学习进步.

eea22f3c3c6b4041b877030e1f87f6e0.png

84722f9e813f5adb05469376f76a0630.png


推荐阅读

(点击标题可跳转阅读)

[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-阿里,网易,滴滴,头条等20家面试真题

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-Javascript条件逻辑设计重构

[自我提升]-送给React开发者十九条性能优化建议

[自我提升]-页面可视化工具的前世今生

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历

觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

e4b1997e23aa23679ae7012fbccdbefd.png   

点个赞👍🏻,顺便点个 在看 支持下我吧

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值