理解计算机与内存

计算机的历史

艾伦·麦席森·图灵

艾伦·麦席森·图灵(Alan Mathison Turing,1912年6月23日-1954年6月7日),英国数学家、逻辑学家,被称为计算机科学之父,人工智能之父

图灵机

图灵机,又称图灵计算、图灵计算机,是由数学家艾伦·麦席森·图灵(1912~1954)提出的一种抽象计算模型,即将人们使用纸笔进行数学运算的过程进行抽象,由一个虚拟的机器替代人们进行数学运算

所谓的图灵机就是指一个抽象的机器,它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的颜色。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序输出信息到纸带方格上,并转换自己的内部状态,然后进行移动

第1代:电子数字积分计算机(巨无霸计算机)(1946—1958年)

普遍认为的的第一代计算机。
一句话,就是用特别大的电子管来进行数据的处理,把每个电子管用线路连接后,来通过不同的连接方式产生不同的数据
两个代表计算机

  • ENIAC
    美国宾夕法尼亚大学研制的人类历史上真正意义的第一台电子计算机,占地170平方米,耗电150千瓦,造价48万美元,每秒可执行5000次加法或400次乘法运算。共使用了18000个电子管
  • EDVAC
    第一台并行计算机,实现了计算机之父“冯.诺伊曼”的两个设想:采用二进制和存储程序

感受下电子管的大小

这个大的物理设备,他却只能存1或者0。

可以看到非常非常多的线,每次需要换计算方法的时候,都需要重新插拔线路组成新的计算模型
虽然方式很原始,但是他就是我们现在计算机中的基本原理

第2代:晶体管数字机(1958—1964年)

也就是把电子管那东西给变小了

第3代:集成电路数字机(1964—1970年)

集成电路,也就是IC,后面会用到IC这个名词

第4代:大规模集成电路机(1970年至今)

也不做过多说明了

计算机的分类

  • 超级计算机
  • 工业控制计算机
  • 网络计算机
  • 个人计算机
  • 嵌入式计算机

超级计算机的CPU一般都会有百来个,超级计算机的应用场景非常多,导弹轨迹,海啸探测,天气预报,天体数据等等

另外较先进的计算机有

  • 生物计算机
  • 光子计算机
  • 量子计算机

从当前的发展趋势来看,量子计算机是比较有可能走入我们的生活的

我们普通使用的计算机,一个比特,例如电子管半导体或者存储位只能保持一种状态,要么表示 1 ,要么表示 0
量子计算机中有一个量子比特,那么他就可以保存多个状态,当然了,他的实现也是非常困难和复杂的,都是物理知识了

假设我们现在有 10000 个数据需要处理

  • 普通计算机在某一时刻只能处理一个,而量子计算机可以处理10000个二进制数,
  • 而量子计算机的数据读写,都是可以同时进行的,也就是一个非常强的并行计算能力

另外还有一个可观的数据,一个四核的Core i7拥有7.31亿个晶体管,那么量子计算机呢?
理论上,300个量子比特可以承载的数据为2的300次方,远远远远超过我们使用的计算机,实际上由于计算过程可能会出错需要1000个量子比特
不过1000个,对于我们来说已经是非常可观了

然而,现阶段还未出现真正意义上的量子计算机,不过已经出现能够计算简单方程式的量子计算机

进制的转换

十进制:

计数符号:0,1,2,3,4,5,6,7,8,9
基数:10
如:

二进制:

计数符号:0,1
基数:2
如:

十六进制:

计数符号:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F
其中A-F为 10-15,总共16个计数符号
基数:16
如:

十进制转二进制:

比如 123:

除以 2 的商(取整) 余数
123/2 = 61 1
61/2 = 30 1
30/2 = 15 0
15/2 = 7 1
7/2 = 3 1
3/2 = 1 1
1/2 = 0 1

然后从下往上即可得到最终的二进制 1111011

二进制转十进制:

就是前面的例子

直接取幂值

二进制转八进制:

从右向左,每三位进行一次转换,由于三位最大的也只有 111 ,所以你在转换时可以使用十进制的转换思想进行三位的转换
比如:
1111111111
按照三位分开则是

  • 1
  • 111
  • 111
  • 111

计算111的八进制为7,则转换后的结果为1777

八进制转二进制:

反过来,直接把八进制的对应的二进制数连接即可

二进制转十六进制:

从右向左,每四位进行一次转换
比如:
1111111111
按照四位分开是

  • 11
  • 1111
  • 1111

计算1111为十六进制的F ,11为3,则转换结果为3FF

十六进制转二进制:

反过来,直接把十六进制的对应的二进制数连接即可
文章结尾附带进制对照图

冯·诺依曼结构计算机

又称存储程序计算机,也是现代计算机的原型和范本

下面是结构图

工作过程(了解即可)

  • 在控制器的控制下,从存储器上取出命令
  • 分析命令,得到计算命令和待操作的数
  • 从存储器上取出待计算的数放入运算器
  • 运算器计算结果
  • 输出到存储器或输出设备

运算器、控制器、存储器的一部分(寄存器,内存)都是放在CPU中

具体存储器的原理可以上网查询

存储器能存得住数据的大致原理:RAM中的两个三极管状态互相稳定,稳定下来,就可以表示是1还是0,读取数据的时候,地址解析器会去列地址译码器,通过交叉的点获取到指定数据

存储器的范围最广,寄存器、内存、硬盘都属于存储器,对于开发者而言,寄存器和内存是比较重要的.

他们的处理速度如下(快至慢):

  • 寄存器(在CPU中,数量较少,十几个,制作成本高,用于存放待操作数、指令和结果)
  • 高速缓存(并不是一定在CPU中,也有在外头,如主板上的缓存,称为外部高速缓存,通常用作数据缓存区)
  • 内存(CPU想放但放不下)
  • 外存(硬盘等等)
    内存又叫内存储器,也就是主存储器,存放CPU中的运算数据,

CPU指令执行

CPU并不是什么程序都能执行的,不可能我写一段程序然后丢给他,他就知道怎么做

指令集

CPU中用来计算和控制计算机系统的一套指令的集合,像Intel X86指令集、ARM指令集等

是在CPU设计的时候就预先定义好的,是体现CPU性能的重要标志

指令

  • 最终表现为二进制
  • 长度随CPU类型不同而不同
  • 包含一个或多个字节
  • 包含指令码 和 操作数
    • 指令码:说明要做的动作
    • 操作数:指要操作的数 或者 地址

比如:1101110111111100 00010011 00010011
前面红色部分就是指令码,后面两个部分就是操作数,操作数可以0-3个

程序的执行

从上个部分可以知道,CPU他只能接收他自己能看懂的指令

1
2
a = 1;
b = 2;

像这样一段代码,编译器先进行编译成汇编代码,再转换为机器码,CPU便能够接收 ,像程序中的一些关键字,return等等都会被翻译成指令
而JAVA是先将JAVA编译成class,再通过JVM来解释字节码翻译成机器语言

内存的物理机制

内存条:包含上亿个元器件,有两种电压 ,0V 和 5V ,5V 是通电 0V是断电
二进制,只有两个数字 0 和 1 ,0表示元器件的断电,1表示通电,每一个值 都代表一个元器件的状态,8个元器件则表示一个二进制,比如:00000000,11111111,

1个元器件称为1比特(Bit)或1位,8个元器件称为1字节(Byte),那么16个元器件就是2Byte,32个就是4Byte

内存实际上是一种名为内存 IC 的电子元件。虽然内存 IC 包 括 DRAM、SRAM、ROMA 等多种形式,但从外部来看,其基本机制都 是一样的。 内存 IC 中有电源、地址信号、数据信号、控制信号等用于 输入输出的大量引脚(IC 的引脚),通过为其指定地址(address),来 进行数据的读写

下图是内存 IC(在这里假设它为 RAMB)的引脚配置示例。虽然 这是一个虚拟的内存 IC,但它的引脚和实际的内存 IC 是一样的。VCC和 GND 是电源,A0~A9 是地址信号的引脚,D0~D7 是数据信号的 引脚,RD 和 WR 是控制信号的引脚。将电源连接到 VCC 和 GND 后, 就可以给其他引脚传递比如 0 或者 1 这样的信号。大多数情况下,+ 5V 的直流电压表示 1,0V 表示 0

那么,这个内存 IC 中能存储多少数据呢?数据信号引脚有 D0~D7 共八个,表示一次可以输入输出 8 位(= 1 字节)的数据。此外,地址信 号引脚有 A0~A9 共十个,表示可以指定 0000000000~1111111111 共 1024 个地址。而地址用来表示数据的存储场所,因此我们可以得出这 个内存 IC 中可以存储 1024 个 1 字节的数据。因为 1024 = 1KA,所以该 内存 IC 的容量就是 1KB

现在的计算机不可能只有这么少的引脚,一般内存IC中会有更多的引脚来存储更多的数据

下图是进行读写时的流程图


内存模型

使用楼层模型来表示内存模型再好不过

每一层,8个位,一个字节一层,1024层,1KB的数据

我们知道 char 占一个字节, short 类型占两个字节,long 占4个字节

那么当我们分别写出下面三个语句时,内存图如下:

1
2
3
char *d;
short *e;
long *f;

可以看出,不同类型占用内存大小是不一样的

字节对齐

为什么需要对齐

  • 各个硬件平台对存储空间的处理不尽相同,比如一些CPU访问特定的变量必须从特定的地址进行读取,所以在这种架构下就必须进行字节对齐了,要不然读取不到数据或者读取到的数据是错误的
  • 会对CPU的存取效率产生影响:比如有些平台CPU从内存中偶数地址开始读取数据,如果数据起始地址正好为偶数,则1个读取周期就可以读出一个int类型的值,而如果数据其实地址为奇数,那我们就需要2个读取周期读出数据,并对高地址和低地址进行拼凑,这在读取效率上显然已经落后了很多了

两个对齐规则

1
2
3
4
5
6
struct MyStruct
{
char a;
int b;
double c;
};

结构体中元素按照定义顺序依次置于内存中,但并不是紧密排列。从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上

首先,char占一个字节,表示为一个方块,int占4个字节,根据规则,int需要对齐,他需要在他的倍数上存放地址,也就是 4,8,12,16这样,double同理

现在我们将代码换一下

1
2
3
4
5
6
struct MyStruct
{
char a;
double b;
int c;
};

这时候的大小,可不是20,而是24,需要用到第二个规则

如果结构体大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾

首先,根据规则一进行字节的摆放,放完之后,就是 8个 字节放 char,8个字节放double,然后4个字节放int
根据规则二,所以结构体的整体大小要为 8的倍数,所以最后填充4个字节

如果结构体中放数组怎么办?
数组其实里面的元素地址是连续的而已

数据类型

我们知道每种语言中的不同数据类型所占的内存大小都是不一样的,他们的取值范围也是不一样的,以C为例:

类型 存储大小 值范围
char 1 字节 -128 到 127 或 0 到 255
unsigned char 1 字节 0 到 255
signed char 1 字节 -128 到 127
int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
short 2 字节 -32,768 到 32,767
unsigned short 2 字节 0 到 65,535
long 4 字节 -2,147,483,648 到 2,147,483,647
unsigned long 4 字节 0 到 4,294,967,295

那么

  • 他的大小是如何计算的?
  • 为什么最大值和最小值不是对应的?
  • 为什么有符号和无符号的范围不一致?

下面做个分析,以int类型的数据为例,下图是两个不同int类型的内存空间图,一个方块代表一位,也就是一个bit

这是基本类型int的内存空间图

这是短整型 short int 也就是 short 数据类型的内存空间图

现在有一个变量,类型为 无符号整型

1
unsigned int i = 666;

十进制的 666 转换成 二进制的值是 1010011010
那么他在内存空间中的表示则是:

如果他是无符号的整型呢?

1
signed int i = -666;

他在内存中存储的结果是这样的:

为什么他的二进制表示和正数不一致呢?

记住计算机是如何存储负数的:先对最高位设置为 1,然后对原码绝对值进行取反,取反后整个 +1 ,得到的二进制就是在内存空间存储的数据,这种操作叫 补码

首先将最高位设置为 1,最高位表示正数还是负数,1为负数,0为正数

然后对除了最高位的所有二进制进行取反

取反后 +1

为了验证结果,我们用C++来输出一个负数的十六进制

1
2
3
4
int i = -666;
cout << hex << i << endl;
//结果
fffffd66

fffffd66对应的就是上面补码后的二进制

网上现在还没有能够计算负数二进制的计算器,反正我没找到,所以负数的转换你在计算器中计算的结果是不对的

最大值与最小值

还是以int为例

对于无符号的 int 类型,把所有位都设置为 1 那就是最大值了,都设置为 0 ,那就是最小值了

所以他的范围是0 —— 4,294,967,295

对于有符号的最大值,除了最高位,其他设置为 1 ,那么就是他的最大值,即 2,147,483,647

那么他的最小值,是不是这样呢 ?符号位设置为1,其他都是设置为1,是否为他的最小值呢?

先计算这个情况下的值是多少,既然最高位表示负数,那把剩下的转换后加个负数,也就是-2,147,483,647,从刚才的范围表格中可以看到他的最小值其实应该是-2,147,483,648

首先,看 0 这个数的二进制,

  • int类型的 0
  • 二进制就是32个 0
  • 取反后是32个 1
  • +1操作后,变成33个,二进制加法逢2进1,也就是最高位为1,后面32个为0
  • 最高位为1,然后丢弃掉最高位,因为他只能存32位啊,所以还是 0

因此0只有一个状态,他的补码和源码是一致的.

那么0后面的最小的整型小数是 -1

  • int类型 -1的二进制表示是32个1,因为前面讲过负数的存储,
  • -2的二进制是前面31个1,最后一个0,
  • 然后一直增加到前面说过的数-2,147,483,647,他的二进制是

这个过程中,可以发现,还有一个状态没用到,就是这个状态

那这可不能浪费,浪费可耻啊.
于是不知道谁做了一个规定,当最高位是 1 ,其他位全是 0 的时候,最高位即表示符号,也表示值,那么上图的二进制转换十进制就是2,147,483,648,然后再给他加个负数,也就是-2,147,483,648

浮点型

计算机的计算问题

前面讲的存储和最大最小值都是基于整型的基础,那么浮点型又有什么不同呢?

比如0.1,0.666这样我们能够直接理解的小数数字,计算中如何存储呢?
肯定不可能像int那样按照范围来,因为那种存储方式已经把所有的状态都用掉了

首先看一个二进制数1011.0011
按照之前的进制转换,小数点前面的十进制为11,小数点后面就开始使用-1次幂来处理,得出的值为1875,全部相加
所以这个二进制对应的十进制为11.1875
这个是二进制带有小数点转为十进制的方式

然而像0.1这种数字,计算机是存不进去的,为什么呢?

首先,按照前面的二进制,小数点后面有4位,因此它的范围应该是 0000 —— 1111

二进制数 对应的十进制数
0.0000 0
0.0001 0.0625
0.0010 0.125

可以从表格中看出,0.1直接被跳过了,二进制虽然看起来是连贯的,但是对应的十进制可不是

实际上,如果你用进制计算机算0.1的二进制的话,它算出来的值是0.00011001100...这样 1100 无线循环,这个和我们整数中 1/3 除不尽一个道理.

由于它无法表示正确的值,所以他只能取近似的值,像我们 1/3取出来的值就是 0.3333, 你让这个数乘以3,是得不到 1 这个结果的

浮点类型

前面说到一个二进制1011.0011,这种我们直接看可以理解,可是计算机并不理解
浮点型基本都有两种, float 和 double ,分别是32位单精度浮点类型和64位双精度浮点类型

浮点型的存储方式

因为这个是二进制,所以这个基数就是固定为 2,所以我们只需关注 符号、尾数、指数 这三个

上图的浮点型内部构造是由IEEE规定的,是一套标准

  • 符号:1表示为负数,0为正数
  • 尾数:将小数点前面 的值固定为 1 的正则表达式
  • 指数:EXCESS 系统表现

看不懂术语没关系,因为下面还有更多术语

尾数

将有多种可能性的浮点数统一的表达式

首先以十进制为例,比如这样:

一个数出现了三个结果,这样处理起来可就麻烦多了

于是,不知道谁做了一个规定:小数点前面是 0,小数点后面第 1 位不能是 0
也就是上面的三种情况,只有第一种情况满足

接下来看二进制:

他的规定就是:将小数点前面的值固定为 1 的正则表达式,注意,是整数部分得我第一位为 1,没有其他数,实际上这个 1 在实际的数据中是不存的
也就是对二进制的移位操作(逻辑移位)

就像这样:

指数

用大白话讲讲这个EXCESS是个啥

假设有5个数,1到5

  • 我现在规定,3 他的值就是 0,别我问原因,这是规定
  • 那么 2的值是什么
  • 是 -1
  • 那么 4的值是什么
  • 是 1

EXCESS就是这种东西
EXCESS系统表现是指,通过将指数部分表示范围的中间值设为 0,使得负数不 需要用符号来表示

来个图,直观感受一下,11111111 = 255/2 = 127.5,小数点遗弃

double的话同理,就是他的范围广而已,11111111111 = 2047 的 1/2, 即 01111111111 = 1023(小数部分舍弃)表示的是 0

下图就是 0.75 转换的流程图

转换流程

十进制小数转二进制

比如 666.625
666为 1010011010

小数处理:

  • 0.625*2 为 1.25 整数部分为 1
  • 0.25*2 为 0.5 整数部分为 0
  • 0.5*2 为 1.0 整数部分为 1

因此它的二进制位 101,注意是从上到下
所以666.625的二进制位 1010011010.101

现在还有一个数,666.66,我们算一算它的小数

  • 0.66*2 为 1.32 整数位 1
  • 0.32*2 为 0.64 整数位 0
  • 0.64*2 为 1.28 整数位 1
  • 0.28*2 为 0.56 整数位 0
  • 0.56*2 为 1.12 整数位 1
  • 0.12*2 为 0.24 整数位 0
  • 0.24*2 为 0.48 整数位 0
  • 0.48*2 为 0.96 整数位 0
  • 0.96*2 为 1.92 整数位 1
    。。。。。。

后面很长很长很长……,他是无限循环的

所以只能截取 ,截取就必然造成精度的损失,具体截取多少位不是很清楚,总之截取23位吧,反正后面做逻辑移位的时候,多余的还是会删除的

于是0.66的二进制为 0.1010100011110101110000101000111101011100001
666.66的二进制就是 1010011010.1010100011110101110000101000111101011100001

流程

十进制小数到二进制

  • 666.66
  • 1010011010.1010100011110101110000101000111101011100001
  • 将小数点前面的值固定为 1
  • 1.0100110101010100011110101110000101000111101011100001*2^9
  • 指数为 9
  • 9 +127 = 136
  • 指数转二进制 136 = 10001000
  • 尾数将整数部分1去掉,就是只取小数点后面的数
  • 0100110101010100011110101110000101000111101011100001
  • 只保留23位,如果不足23位就后面补 0 补到23位
  • 01001101010101000111101
  • 组合
  • 符号位 + 指数 + 尾数
  • 于是结果就是 0 10001000 01001101010101000111101

二进制小数到十进制

  • 0 10001000 01001101010101000111101
  • 符号位 为 0 指数为 136-127 = 9,尾数为 01001101010101000111101
  • 为尾数补上1 ,真实尾数为 1.01001101010101000111101
  • 组合
  • 1.01001101010101000111101*2^9
  • 1010011010.1010100011110101110000101000111101011100001
  • 666.6599999999999682
  • 某些语言可能会做四舍五入处理
  • 666.66
代码测试

第一种方法:输出它的二进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main(int argc, const char * argv[]) {
float data;
unsigned long buff;
int i;
char s[34];
// 将 0.75 以单精度浮点数的形式存储在变量 date 中。
data = (float)666.66;
// 把数据复制到 4 字节长度的整数变量 buff 中以逐个提取出每一位。
memcpy(&buff, &data, 4);
// 逐一提取出每一位
for (i = 33; i >= 0; i--) {
if(i == 1 || i == 10) {
// 加入破折号来区分符号部分、指数部分和尾数部分。
s[i] = '-';
} else {
// 为各个字节赋值 '0' 或者 '1'。
if (buff % 2 == 1) {
s[i] = '1';
} else {
s[i] = '0';
}
buff /= 2; }
}
s[34] = '\0';
printf("%s\n", s);
return 0;
}

输出的结果为 0-10001000-01001101010101000111101 ,和我们上面的分析结果一致

第二种方法:直接输出它在内存空间的十进制

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
float a=666.66;
int *p=(int *)&a;
printf("%d\n",*p);
return 0;
}

结果为1143384637,可以用进制计算机计算它对应的二进制,转换后为1000100001001101010101000111101,前面的0被忽略了,不重要.

如果是double的话,就是位数不一样而已,原理都是一致的
另外还有一个精度问题,它们的精度是由尾数来决定的,所以

  • float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字
  • double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位

附录

进制对照图

进制对照图