(二)计算机如何保存字符和数字,并完成计算

目录

 

一、字符存储和解析

1.1、初步分析和设想

1.2、引入字符集和编码

1.3、编码实现细节

1.4、总结

二、数字存储和运算

2.1、整数存储

2.2、整数运算

2.3、浮点数存储

2.4、浮点数运算

2.5、总结

三、理解加法器的电路实现


一、字符存储和解析

1.1、初步分析和设想

  • 背景

计算机体系内的存储设备,单个bit只具备存储0或1的能力。所以任何信息在计算机上都是通过一串0和1表示。

  • 初步设想

对于字符,我们可以认为它是一个有限的集合。我们只要能把全世界所有用到的字符全部收集起来,并且进行整理,让每一个字符都有一个不重复的编号。这样我们计算机只要存储序号对应的数字,就相当于间接存储了字符。

  • 难点:
  1. 我们每个字符用多少个字节表示?太小了,世界字符太多没法表示,同时不具备扩展性;太多了,占用空间太多。
  2. 不同字符用的字节数一定要一样么?如果不一样,我们拿到存储后的二进制串,怎么才能解析每个字符到底占用了几个字节呢?
  3. 怎样才能让我们使用的存储空间更少呢?

1.2、引入字符集和编码

基于以上的理解,我们直接分析前辈们如何实现。我们以当前最流行的unicode为例。

  • 字符集

我们说的unicode表示字符集,里面收纳了全球所有字符,并且每个字符都有唯一的编号,称之为码点或码位

  • 编码

编码可以理解为,把码点转换为特定格式的二进制存储,后续需要读取字符时再把存储的二进制以同样方式解析成码点

  1. UFT-32,我们用固定四个字节来表示每个字符,很明显,如果对于只是用英文的机器来说非常的浪费。因为英文字符在unicode字符集的最前面。这些英文字母对应的码点只需要一个字节就能表示。
  2. UFT-8,一种变长的编码方案,使用 1~6 个字节来存储。这种方案允许不同字符用不同长度的字节数来存储。这种方案有一个明显的优点,无论你想用到的字符在unicode的码点是大还是小,它都会选择一个合适的字节数来存储。那么它是如何存储,以及存储后如何解析呢?我们后续分析
  3. UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。

1.3、编码实现细节

 接下来我们解释编码格式的具体实现

  • UTF-32很好理解,固定长度的字节存储和解析都非常的容易。
  • UTF-8,这种编码方式的目的很明显,尽可能使用少的字节来存储字符。但是一旦不同字符使用的字节数不一样,我们在拿到二进制串解析的时候就会手足无措。所以我们必须让存储好的二进制串具备一个特定的格式,既能节省存储空间又能让我们可以解析。

例如存储的二进制串有如下格式:
0xxxxxxx:单字节编码形式,这和 ASCII 编码完全一样,因此 UTF-8 是兼容 ASCII 的;
110xxxxx 10xxxxxx:双字节编码形式;
1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式;
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式。

这里列举了一个字符分别用1-4个字节如何表示,在这个二进制串中,真正存储码点的是xxx这些二进制,而这些110,1110等的二进制就是我们的特定格式,为我们解析码点提供帮助。

我们按顺序解析存储在内存中的二进制串,当遇到当前字节是0开头,则表示这个字符占一个字节,其码点是后面的7个bit位表示。同样其他的字符也可以按这种方式进行解析,获取字符占用字节数,以及它对应的码点。

可能大家有个疑问,如果有个字符的码点刚好和我们的格式开头一样,岂不是会解析错误。其实这个存储的格式设计的很巧妙,可以避免这种出现这种问题,大家可以自己去验证。

  • UTF-16, 这种编码方式分两种情况

如果字符的码点在 0 ~ FFFF 之间,直接把码点转为二进制存储即可。

如果字符的码点在 10000~10FFFF 之间,则使用类似于UTF-8的格式进行存储:110110xx xxxxxxxx 110111xx xxxxxxxx

解析二进制串的时候只要去判断当前四个字节的格式是否满足110110xx xxxxxxxx 110111xx xxxxxxxx,如果不满足则表示当前字符占两个字节,否则占四个字节。

UTF-8是因为存储格式比较完善,可以避免把码点的二进制串当成前缀解析。但是UTF-16很明显没有这个能力。如果有两个字符的码点分别如下:11011010 00000000 ,11011110 00000000,当他们刚好存储在一起时,那岂不是被解析成一个字符。所以这个只能从字符集上处理,那就是字符集里不许使用 11011000 00000000 到 11011111 11111111 之间的码点,换成十六进制就是:0xD800~0xDFFF。而实际上也确实如此,Unicode 字符集这个区间内确实没有收录任何字符。


1.4、总结

我们现在已经完全理解了unicode字符集以及它目前可使用的三种编码方式。此时我们再来看经常听说的 ASCII, GBK, GBK2312,应该也就很好理解了。他们都是自成一套字符集和编码方式。


二、数字存储和运算

计算机内,加减乘除,都只需要通过 位移 + 加法器 来实现。接下来我们来具体分析是如何做到的呢。

2.1、整数存储

整数可以分为:有符号整数(正整数,负整数),无符号整数(都是正整数)

符号位是最高位,最高位为0表示正数,为1则表示负数
无符号的正数则不需要符号位,由于多了一位可以表示数值,所以能表示的值更大

负整数是一个比较特殊的存在。它在计算机中有三种形态:源码,反码,补码。那我们在计算中要以哪种形态存储负整数呢?为什么要搞这么复杂呢?
负整数在计算机中一般以补码的形式存储,之所以有几种形态,我认为每种形态可能都有他的作用,例如因为对于人而言,源码好理解,而对于cpu的运算器而言,补码更方便运算。如何运算后续会讲到。
反码和补码是针对原码而言,它的规则如下:

  1. 负数的反码,在源码的基础上,符号位不变,其它位取反
  2. 负数的补码,在反码的基础上,最低位+1

2.2、整数运算

下面用到的都是有符号整数,为了方便举例,我们假设每个整数占一个字节。

  1. 正数+正数正常求解即可
  2. 负数+负数正常求解后最高位强行制为1即可
  3. 正数+负数求解过程如下,先计算 正数补码+负数补码 得到结果A(超出最高位溢出的部分被忽略),此时的结果A是补码,把A还原成原码即为最终的结果。
  • 负整数的加法

5 对应的源码:0000 0101

-5 对应的源码:1000 0101,补码:1111 1011

5 + (-5) = 0   

0000 0101(5的源码) + 1111 1011(-5的补码) = 0000 0000

这就是补码的作用,是不是很神奇。我们既然能实现负数的加法,那其实就已经实现了减法了:5 + (-5) = 0   不就是  5 - 5= 0 。所以对于减法,我们可以转换为加法来实现。

  • 正整数的乘法

通过位移 和 累加的方式实现乘法

13 * 9 = 117

0000 1101 * 0000 1001 计算过程如下

  • 正整数的除法

位移 和 减法 实现除法

实现细节暂时不清楚。


2.3、浮点数存储

较为复杂的是浮点数的保存,例如32位单精度浮点数保存格式如下:

该浮点数最终的值则为:

可以看到有效位数其实真正是 24位,因为默认最前面会有一个1。

其中e能够表示的整数空间,就是 0~255。我们在这里用 1~254 映射到 -126~127 这 254 个有正有负的数上。因为我们的浮点数,不仅仅想要表示很大的数,还希望能够表示很小的数,所以指数位也会有负数。而0和255则有特殊用处,如下:

0.1-0.9,计算机只能精准的表示0.5,即2的-1次方。由此可以得知计算机能精确表示的小数只能是 2的-n次方的多个结果的和(1/2+1/4+1/8类似这种组合)。所以0.3,0.4这些无法精确表示的小数,计算机只能通过近似值来表示,近似到直到把有效位数填满。例如二进制的小数 0.1001 表示为十进制则为 2的-1次方 加上 2的-4次方。若要用二进制表示十进制的0.1,则用上面的方法一直往下累加,知道有效位数用完,但最终还是近似值。经典的例子:0.3+0.6 = 0.89999999...


2.4、浮点数运算

  • 两个浮点数相加,先把指数位变成一样,再把有效位的数相加。指数位小的向指数位大的靠拢,意味着指数位小的那个浮点数需要把有效位向右移动。这样一来,指数位小的数据在进行相加时就可能丢失精度。例如 20000000.0f + 1.0f = 20000000.0f

下面是解决浮点数相加丢失精度的算法,但是还是避免不了极端情况的精度丢失。

    // i=16777216, sum=16777216, 结果正确
    // i=16777217, sum=16777216, 结果错误(两个数指数位差出23位,对齐后小的那个数有效位全部失效:1f+16777216f=16777216f;)
    // i=16777218, sum=16777218, 结果正确(本例中把1f变为2f,指数位的差距立马小于23位,对齐后最左边的有效位保留了:2f+16777216f=16777218f)
    public static void main(String[] args) {
        float sum = 0f;
        float remainAdd = 0f;

        for(int i=0; i<16777217; i++) {
            float add = 1f;
            float needToAdd = add+remainAdd;
            float nextValue = sum + needToAdd;
            remainAdd = needToAdd-(nextValue-sum);
            sum = nextValue;
        }

        System.out.println(sum);
    }
  • 例如mysql中的decimal类型的数据,保存的不是近似值,而是采用准确保存方法,若要精确的数据则可以使用该类型。

2.5、总结

还有很多关于浮点数的乘除法,无符号数的运算,都比较复杂,有兴趣可以自己去了解。


三、理解加法器的电路实现

计算机硬件层面能做的事情非常有限,就是最基础的门电路:与门,或门,非门,异或门。

在基础门电路智商,我们来实现单个bit位的加法运算。A和B两个bit相加,会产生一个进位U,一个当前位 S 的结果。

由于二进制的特殊性,两个bit相加,只有当两个bit都是1的时候才能进位。那么判断是否产生进位可以通过 与门 得到

而一个bit只有0或1两种可能, 两个bit相加只有四种情况(0+1,1+0,1+1,0+0),可以发现相同的bit相加后当前bit位都为0,不同的bit位相加后则为1。那么当前bit位结果则可以通过 异或门 得到

可以发现一个异或门+一个与门可以完成两个bit的加法运算,我们称作半加器,如下图

若两个数都拥有多位bit,相加时低位产生的进位数据会参与高位之间的求和,此时则需要接收低位传过来的进位数据。我们可以用两个半加器+一个或门实现。第一个半加器还是计算当前位的加法,产生一个进位U,一个当前位 S 的结果。第二个半加器则把当前位结果 S 和低位传过来的进位 U0 相加得到一个进位 U1,一个当前位 S1 的结果(最终的当前位结果)。最后把两个半加器产生进位进行或操作,得到最终的进位结果 U2。我们称这位全加器,如下图

把全加器组合串起来就可以得到加法器,对多位bit的数据进行加法运算。通过补码运算,可以把减法运算变成加法运算;而乘法可以用加法来做,除法可以转变成减法。这样一来,加、减、乘、除四种运算“九九归一”了。这对简化CPU的设计非常有意义,CPU里面只要有一个加法器就可以做算术运算了。通过加法器我们就可以得到算术逻辑单元(ALU)

乘法的电路可以通过加晶体管,设计更复杂的电路来达到并发。
除法的电路却不好实现并发,所以除法执行可能需要几十个时钟周期,比乘法更久。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值