计算机组成原理——计算机系统+信息处理与表示

简单总结一下以前学过的一些基础课程的内容,真是时时看,时时忘(误

一、计算机系统介绍

1.处理器结构

Von Neumann arch. & Harvard arch.

冯诺依曼体系结构: 一种将程序指令存储器和数据存储器合并在一起的存储器结构,该体系计算机由五大基本部件组成:

  1. 存储器(Memory):用来保存和记录原始数据、程序和运算结果的部件
    在这里插入图片描述
  2. 运算器(ALU:执行算术、逻辑运算单元,一般大部分位于CPU中)
  3. 控制器(Controller:发出控制指令部件)
  4. 输入设备(Input device):用来往计算机中输送程序、数据的装置
  5. 输出设备(Output device):将计算结果输送出来的装置
    在这里插入图片描述
    其中传递信息的公共通道为Bus(总线),系统总线分为三种:控制总线、地址总线、数据总线。
    在这里插入图片描述

我们通常使用的储存程序型电脑一般都是冯诺依曼结构,指令存储和数据存储存放在同一个存储器的不同物理位置上,结构简单,好设计。

采用二进制形式表示数据和指令,指令由操作码和地址码组成(汇编语言)

其指令的执行是顺序的,即一般按照指令 在存储器中存放的顺序 执行,程序分支由转移指令jump一类实现。

Harvard arch

哈佛结构是一种将程序指令存储 和 数据存储分开的存储器结构设计概念,这里由于接触的少,不做深入讨论。其将程序指令存储和数据存储分开,虽然成本提高,但是增加带宽。
在这里插入图片描述

一些基本概念:

  1. Program:完成一项任务所需的并按照一定顺序排列起来的一系列 Instruction
  2. Instruction :给机器下达的完成一项基本操作的指令(不同处理器架构通常具有不同的指令集),一般带有 [操作码] [地址码] …
  3. Register Files:寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果 (造价贵且读取速度快)

C程序的生命
举个例子:cpp文件如下

#include<stdio.h>
int main(){
	printf("Hello,world!\n");
}

当程序编好以后,它作为源程序被存在存储器中,形式是解释为Ascii表映射字符的文本文件:
在这里插入图片描述
之后我们编译运行这个源程序的过程经过几个阶段(针对c文件):

  1. 预处理器阶段:根据#include一类的修改hello.c (文本) 为 hello.i (文本),宏定义在这里被解释
  2. 编译阶段:将修改后的hello.i 通过编译器cll 等编译为hello.s 汇编程序(文本),inline函数在这里被选择性展开(看编译器)
  3. 汇编器阶段:将hello.s通过汇编器as一类进行可重定位等操作,变为hello.o (二进制)
  4. 链接阶段:由于hello.c中调用了printf函数,需要进行链接,这个阶段link printf.o 最终生成我们可以执行的可执行文件(二进制)–windows下为hello.exe ,Linux下为a.out(默认)

在这里插入图片描述
---------------------------分隔符-------------------------
典型系统的硬件组成:
如下图,包括:CPU、I/O device、Main memory、Bus
在这里插入图片描述
结合上面的hello.c的生命过程,在硬件上:

  1. 用键盘+鼠标执行hello命令
    在这里插入图片描述

  1. 从disk中读取可执行hello
    在这里插入图片描述

  1. 显示hello,world到Display上
    在这里插入图片描述

二、信息处理和表示

1.信息存储顺序(字节顺序)

我们经常用到的int数据类型,在不同的计算机可能表示的字节顺序会不一样,分为大端序、小端序:
所谓大小端,指的是低地址存放的是高位还是低位,如下:01为高位,在大端是低地址存放,67为低位,在小端是低地址存放。
在这里插入图片描述
如何查看int的4个字节地址,可以做一些规避,示例如下:

#include<cstdio>
using namespace std;

int main(){
     int num = 0x12345678;
     char* c = (char*)&num; 
     for(int i = 0; i < sizeof(int);++i){
          printf("%.2x ",*(c+i)); //16进制输出
     }
     return 0;
}

以下是在Windows X86下运行的结果,看起来是小端:
在这里插入图片描述
Sun好像是大端的,这个我没验证···
需要注意的是,char这种1字节的数据类型,没有大小端一说。
顺便补充一下不同字长大小的计算机,C++数据类型的大小区别:

在这里插入图片描述
注意char* 这类其实是表示: 指针地址,跟计算机字长一致。

2.位操作

一般位操作指以下六种:
| for Or
& for And
~ for Not
^ for eXclusive-Or(异或) ----- 可以理解为不进位的二进制加法
<< >> 移位

需要注意
| 和 & 与 || 和 && ,后者是断路判断 (就是不一定逻辑两边都会执行,例如 (X && 5 / X) 这个式子永远不会除0)

<< >> :
左移逻辑、算术左移一位,左边补0;
右移有区别,逻辑右移不带符号(操作数最高位)位移,算术右移带符号右移(举个例子:负数 / 2 还是负数),一般右移都指的是算术右移

危险: 移位大于CPU字长(比如 : x >> 65),一般计算机处理会对 移位 mod 字长;
提醒: 如果对于位操作的优先级不太熟的话,别省 () !

位操作效率非常高,很多地方用位操作能够加速(只不过理解起来可能有点不太适应)
常见的位操作优化和思想:

  1. N取模2,4,8,16… 可以转化为 N & 2 , N & 4 …
  2. 掩码计算: x & 0xff 取后八位等等····
  3. x = x * 10 等价为 x = (x << 1) + (x << 3);
  4. x = 2 * x + 1 等价为 x = x << 1 | 1;
  5. 取一个数的二进制最后一个1: N & -N ( …100000 & (…011111 + 1) )
  6. 判断一个数是否为-1 : ~N (若为-1取反为0)
  7. 一个数组里有N种数,其中N-1种它们出现次数为偶数,只有唯一一种出现奇数次:
    find_num = find_num ^ arr[i] …因为A^A = 0,最后剩下的一定是所求
  8. 快速交换x,y:*x = *x ^ *y; *y = *x ^ *y; *x = *x ^ *y; (也是利用A^A = 0)
    在这里插入图片描述

3. 整数和浮点数的表示

整数操作

编码:

整数分为有符号和无符号数(C++,java好像只支持有符号),其值和二进制对应关系:
在这里插入图片描述
对于unsigned int ,其范围为 0 ~ 2^32-1.
对int ,其范围为 -2^31 ~ 2^31-1. 负数范围比正数多1,因为1 000…000 多了出来(其补码等于自己,除了它还有0也是 ),根据补码定义代表-2^31 + 0
说明:负数取负一定是正数吗?—错!有特例2^31

#include<cstdio>
using namespace std;
int main(){
	int x = -(1 << 31);
     printf("%d\n",-x);
}

在这里插入图片描述

对于无符号数和有符号数,它们之间互相计算时可能会有隐藏的BUG:
因为C++中有符号与无符号计算时,隐性将有符号转化为无符号数(因为无符号数正数范围大于有符号,转化会有问题)
但是这种转换是基于有符号是正数的情况,会把负数转化为正数,导致一些bug:

#include<cstdio>
using namespace std;
int main()
{
	printf("%d",-1 < 0u);
	return 0;
}

结果为false:
在这里插入图片描述
所以不推荐使用无符号数时,参杂有符号int的计算!

整数运算的危险

溢出问题:
加法:
可能出现这种情况: x > 0 && y > 0 but x + y < x && x + y < y
这里只讨论有符号,无符号类似:
正溢出: x + y = x + y - 2^w
负溢出: x + y = x + y + 2^w
例子:

//二分
while( left <= mid){
	int mid = (left + mid) / 2; //加法可能溢出
	//更改成 int mid = left + (right - left) / 2;
	....
}

做乘法的时候 : x * y 得到高于w位的结果,截断剩后w位,导致计算出错
例子:

//求欧拉函数
voit Eular(){
    ...
    Eular[j] = Eular[j] * (i-1) / i;//乘法溢出风险,对于int来说5W * 5W就溢出了
    //更改为 Eular[j] = Eular[j] / i * (i-1) ;
}

做除法
由于除法操作是用位移实现的,所以int除法是截断小数部分的,并不是向下取整哦~

#include<cstdio>
using namespace std;
int main(){
     printf("%d",-6170/8); // -771.25
}

在这里插入图片描述

浮点数操作

浮点数编码

限于二进制编码,浮点数的表示一般为 :
在这里插入图片描述
这种表示不能很精确的表示小数,只能精确表示,x / 2k,而且一个2k一位很耗空间。

IEEE Stardard 754

其规定浮点数的表示如下:
在这里插入图片描述
其中S是符号位,M代表尾数为[1,2) or [0,1),E代表阶码
对于 4 byte 的单精度float来说,其字节表示为:
在这里插入图片描述
双精度double则为:
在这里插入图片描述
一般右边为0bit(小)
根据exp的取值,E也有变化:

  1. Normalized 规格化 (exp != 0 && exp 不全为1):
    E = exp-Bias(2k-1 - 1,k = exp长度,这里k=8,bias=127,不均匀),
    此时frac位被解释为[0,1)的小数值,M = 1. f f fn-1 f f fn-2… f f f 0
    例如: 在这里插入图片描述
    E = 0b1101 = 13 其代表的值为 (-1)0 * 213 * 1.1101101101101(刚好13位) = 15213.0

  2. Denormalized 非规格化(代表0附近的值)其中exp=0
    此时E = 1-Bias(常数,均匀)
    M = 0. f f fn-1 f f fn-2 … f f f 0

  3. 特殊值(exp 全为1)
    此时若frac = 0,代表INF(无穷:两大数相乘 or 除0 等,代表溢出)
    frac != 0,表示NaN(not a number,例如sqrt(-2.0) )

简单demo看一下特殊情况:

#include<cstdio>
#include<math.h>
using namespace std;
int main(){
     printf("%f %f\n",-1.0/0.0,sqrt(-1.0));
}

在这里插入图片描述

总结就是:(下图为float)
在这里插入图片描述

浮点数舍入
  1. 先比较符号位
  2. NaN 大于任何其他数
  3. 浮点数的判断相等由于精度误差,我们一般使用 abs(x-y) < EPS 来判断
  4. 舍入,浮点数用近似的浮点数来表示实数,采用大于中间值向上,小于中间值向下(四舍五入),等于中间值向前一位趋于偶数舍入
    在这里插入图片描述
    比如: 10.111 舍入到小数点后两位,第三位开始后面为100…是中间值,前一位为1,那么向偶数舍入,UP把1变为0,而10.101也是向偶数舍入为10.10
    而10.0011 最后的110…大于中间值100…UP舍入,10.00011类似小于中间值down舍入
    为什么在等于中间值的情况下需要向偶数舍入(向零舍入)?
    为了平衡误差,统计学上一般分散舍入方向(既向上又向下),使其平衡分布。

实际上,在浮点运算时就会进行这种舍入:
x + y = Round (x + y)
x * y = Round (x * y )
所以浮点数的结合律不成立! :(3.14+1e10) - 1e10 != 3.14
代码测试:

#include<cstdio>
using namespace std;

int main(){
     if((3.14 + 1e10 - 1e10 == 3.14)){
          printf("相等");
     }else printf("不相等");
     if((1e10 - 1e10 + 3.14 == 3.14)){
          printf("相等");
     }else printf("不相等");
}

结果为:
在这里插入图片描述

关于浮点数进行加法和乘法操作,这里简单说一下,因为不是太重要:
乘法: (-1)S1M1 2E1 * (-1)S2M2 2E2
规则为:S1 ^ S2; M1 * M2 ; E1 + E2
E1+E2,可能会导致溢出,
M1 * M2 >= 2时(不满足M 为[1,2) or [0,1)),右移M1 * M2 使得其满足,然后这样会导致E增加
加法类似略…

4.精度转化

  1. int ----> float : 不会溢出,因为float的表示范围远远大于231(其E能表示的范围在27 = 127 左右),这一个转化过程也充分否定了我之前的错误:(类型转化不改变值,只是让编译器换个角度来看同样的字节 )----------事实上,会修改数值与位模式!
  2. int,float ----> double: double的M有52位,可以保留精度值
  3. double ---->float: 精度变小,float可能溢出或被舍入
  4. float,double ---->int: 截断小数部分(向零舍入),并且可能会溢出,因为浮点数表示范围大,可能找不对近似的int表示)

好,就先总结到这里,后面有时间再补充补充, 如有疏忽还望指正~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值