数字的机内表示

Posted zkccpro

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数字的机内表示相关的知识,希望对你有一定的参考价值。

数字的机内表示

先声明,下面的图来自于B站up主——九曲阑干 的视频课程,讲计算机组成原理十分简练通透!想学CSAPP的小伙伴可以看书+看他的视频!本文来自对视频内容的整理加上一些自己个人的逻辑和理解。

数字的机内表示,几乎是每个电学专业的必修课(比如说自动化的微机原理,哈哈哈哈),计算机也不例外。但是本科的时候还真没弄懂这部分知识,所以今天决定再次学习它!

一、 信息在计算机中的存储

首先要弄清楚计算机是如何存储信息的呢?

  • 位和字节

    位(bit)和字节(Byte)的概念应该很熟悉了,考过计算机二级的都知道,哈哈哈。1B=8bit,计算机的各级别存储单元(寄存器、各级cache、内存、外存。。。)都是以bit作为最小单位来存储的。或许不同的存储元件用来表示一个bit的物理元件不尽相同,但物理上的存储原理并不是我们的重点。他们的抽象都是一样的嘛,这一概念相信非常好理解。

  • 字和字长

    这个概念或许一部分人看了有点似曾相识,一下子还不能准确地说出来。字和字长的概念不仅只和存储信息有关了,还与CPU怎么利用存储信息有关。

    我们都知道,CPU是通过各级cache,最终经过CPU寄存器得到外部存储的信息,搬运到CPU内进行计算。可是CPU一次能处理计算多少数据呢?**这个就是字长的概念:CPU一次能并行处理的二进制数据位数。**一个字,就是计算机的字长的长度。不同的计算机是不一样的,比如微机原理中接触的英特尔第一代处理器——8086就是8位的;现在我们熟知的32位或64位计算机,指的就是字长。

    计算机的字长,直接决定了两方面指标:

    1. 最大虚拟内存空间:

    图1 字长与最大虚拟内存的关系

    啥是虚拟内存空间啊,,,回顾一下操作系统方面的知识,就是从磁盘最多搬运多少数据到物理内存嘛。。说白了就是支持的最大内存大小嘛!微机原理或计算机中有一个名词叫,最大寻址空间。和这个是等价的。

    为啥字长和最大虚拟内存(最大寻址空间)有这种关系呢?也好理解,是因为字长决定了地址总线的最大位数,比如m位字长的计算机只能一次处理m根地址总线的内容;而m根地址总线明显能覆盖2^m位的地址嘛。因此就有了以上的关系。

    1. CPU寄存器大小:

      图2 32位与64位计算机的寄存器大小对比(当然这些不是x64规范全部的通用寄存器)

      如图,最外面的绿色寄存器(%rax,%rbx…)就是64位CPU独有的,而红色的寄存器(%eax,%ebx…)是二者共有的,他们之间的包含关系如是。而最里面的蓝色和黄色是更早期的遗留产物啦,如果以后出来一个“128位计算机”,那又要套娃了,目的就是为了兼容旧版本的程序!

      这个怎么理解呢?举个例子,我们都知道linux中,程序可以编译为32位或64位可执行文件:


图3 32位和64位程序

 从寄存器的角度上理解就是,汇编产生的汇编语言采用不同的寄存器组织,`-m32`将会完全采用32位体系下的寄存器组织(%eax,%ebx...),而`-m64`则会采用64位体系下的寄存器组织(%rax,%rbx...)。看图2,64位体系下也有32位的所有寄存器,因此`-m32`编译出来的程序明显也能跑在64位机器上,而反过来则不行!这就是”向下兼容“。

这样,就算比较透彻地搞明白了计算机的存储原理,那么在此基础上可以看看计算机是如何存储整数和浮点数的吧!

首先我认为,“搞清楚计算机是如何存储整数和浮点数”这个议题对程序员来说还是十分重要的,否则你写的程序可能莫名其妙溢出、结果和预期不符。。。等等一些莫名其妙的错误就会逼着你不得不有空的时候彻底恶补大学本科欠下的债。(哈哈哈哈哈,比如说我!微机课上睡大觉,写程序时头挠秃。)

二、整数的机内表示

1. 原码、反码与补码

这三个名词你一定隐约听到过,这仨貌似都是表示整数的一种二进制编码。这没错,但是这三者有啥关系呢?接下来我们深入搞懂这个问题!

我们都知道机内整数有两种:有符号整数、无符号整数,这两种整数的解析方式是不一样的:

  • 无符号整数:

    这就比较简单了,考计算机二级的时候做过十进制转二进制吧,就是这个,转出来是啥就是啥,计算机内部就是这样表示无符号数的。我们换一个名词,对无符号数而言,所有的二进制表示序列都是原码。但无符号可不能表示负数!后面就会看到如果用无符号类型存储负数,会发生什么!

  • 有符号整数:

    有符号整数是如何表示正数和负数呢?有下面3个原则:

    1. 正数和负数都用补码表示

    2. 负数补码=最高位符号位 and 其他位反码+1

    3. 正数补码等于原码

    第二条是表示负数的精髓,反码就是按位取反的意思啦,很好理解;负数符号位为1,正数符号位为0。

    这样,你就可以很轻松地写出一个负数的补码表示了:比如,-5的补码是什么?

    最高位为1,其他位为:101->010+1->011。拼在一起就是:1011。没错,这就是有符号数-5在机内的表示。

    看到这你可能会问,哎,这方法对计算机来说貌似执行起来更费劲啊啊!!!要解析符号位,又要按位取反,又要+1,而且表面看也避免不了乘以-1啊。。。

    我一开始也不懂,但直到彻底理解了这个公式,才体会到计算机科学家们的聪明之处(反正我是想不到这么聪明的方法,可能有不少人也可以想得到吧哈哈哈):

    图4 有符号数补码的实际推算过程(计算机做的事情)

    发现厉害之处了吗?计算机计算一个补码,实际上是把最高位的权重变成了-1而已,次高位开始按照无符号数那样计算权重。神奇的是,结果和符号位and其他位反码+1那个思路的推算结果是一样的,不信你自己试试!

    如此,CPU计算补码实际非常简单,完全和正数一样的效率。完全没有任何解析、多余的乘法步骤。唯一的缺点就是符号位使得数字的表示范围比无符号数少了一半。但仔细想想这也是不可避免的啊,毕竟表示数字的量其实没变,信息量是等价的,其实这根本算不上缺点哈哈哈!

    我相信,讲到这个份上,你现在应该彻底懂了计算机是如何表示一个整数的了。那么,下面这些曾经让你记不住的图,相信在理解原理的基础上,也变得容易记了:



图5 c/c++常见数字类型的机内占用空间及实际表示范围

这里唯一要注意一点是,long类型数据的占用空间和计算机的字长一致的:32位机就占用32位(4B),64位就占用64位(8B)。

2. 不同整数类型之间的转换规则

懂了上面的表示原理后,接下来看一些实际编程中可能会遇到的情况(错误):

  1. 有符号赋值给无符号,或者反过来:

    int a=-2;
    uint32_t b=a;
    std::cout<<b<<std::endl;
    

    结果:

    zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_init.cc -o test_init && ./test_init
    4294967294
    

    学习本节内容之前,看到这个结果的我一脸懵逼,只知道不能这么干,但确实说不出个所以然来。但今天之后,直到了机内表示有符号和无符号数的方法,就明白为什么会这样了!甚至你可以自己动手推算一下,是不是这个数字,记住,赋值过程中,二进制码是不变的;改变的是”解码方式“!

  2. 多位整数赋值给少位整数,或者反过来:

    多位整数赋给少位整数,发生取模后高位截断丢弃,只剩低位,由于取模了,所以对正负号没影响:

    int32_t a=-12345678;
    int16_t b=a;
    std::cout<<b<<std::endl;
    

    结果:

    zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_init.cc -o test_init && ./test_init
    -24910  # 仍可保留其符号
    

    反过来则是高位补0,没啥影响。

  3. 大端到小端,或者反过来

    这一点在音视频处理领域中常用,因为音视频业务对网络编程比较看重。如果是其他后端分布式业务,对网络编程其实并不常用。在网络环境中,大小端的不同也会造成意向不到的错误,不同的大小端性质机器会从高位或者低位开始解析数字,如不注意转换,可能造成困扰。

  4. 两个不同表示的整数运算,准则是:

    把有符号的转换成无符号的;

    把少位的转换成多位的。

    int32_t a=-1;
    uint32_t b=2;
    if(a>b) std::cout<<"a>b"<<std::endl;
    else std::cout<<"a<b"<<std::endl;
    

    结果是:

    zkcc@LAPTOP-OHBI7I8S:~/mytest$ g++ test_init.cc -o test_init && ./test_init
    a>b
    

    意外不,为啥啊?因为进行比较运算前,先把有符号数-1转换成无符号数了,-1补码的最高位是1,是一个很大的无符号数,所以明显在无符号数中,a>b。

希望看完以上介绍的内容,你日后会对编程有一个更深的理解!

三、浮点数的机内表示

这部分不是很重要,大概知道怎么存的就行啦。

浮点数就是小数嘛!小数应该怎么在计算机里存储呢?IEEE-754标准利用了数字的科学计数法:

比如,10进制数:12345.678=1.2345678×10^7;

同理,2进制数:101.010=1.[01010]×21

如此,只需要表示:01010 (尾数,frac)和 2(阶码,exp),再标识号正负数(符号位)即可!

图7 浮点数的机内存储

尾数越长,精度越高;阶码越长,表示范围越大。

所以推算:

  • float的表示范围:[-2^128, 2^128]

  • float的表示精度:2^23->小数点后7位数

  • double的表示范围:[-2^1024, 2^1024]

  • double的表示精度:2^52->小数点后16位

看到这个结果你可能有点迷惑:int表示范围才[-2^31, 2^31-1]。float和int明明位数一样,怎么表示范围大这么多??这不符合能量守恒啊!emmmm,直观的结果忽略掉了一个问题:**浮点型数明显不能表示其表示范围内的[每一个小数]。**实际上,float和double都是越到表示范围的两段,能表示的数越稀疏。到最后可能每跨越100多个整数才能表示上一个小数呢。。。

所以,这给我们一个使用时的启示:如果你要用的浮点数都是绝对值比较小的,那么用float就足矣,绝对值较小的情况下float和double的精度差不多,还节省了4字节;除非你要用的浮点数绝对值较大,此时用float可能会损失精度,应该用double。

最后 举个栗子:将176.0625表示为符合IEEE-754标准的单精度浮点数。

首先将176.0625化为二进制:10110000.0001

  1. 尾数规格化:1.01100000001×27,因此尾数域求出来了,为01100000001
  2. 阶码的移码:7的二进制为00000111,移码为10000110。
  3. 拼接:0 10000110 01100000001000000000000
  4. 尾数域记得要补0使尾数有23位。

  1. 2 ↩︎

以上是关于数字的机内表示的主要内容,如果未能解决你的问题,请参考以下文章

数字的机内表示

软考程序员要看哪些书?

具有较高尾数的fp如何表示较小的数字?

verilog数字高位扩展表示方法

python中怎么表示是3的倍数或者尾数是3的数?

具有2或1的补码尾数的浮点系统是否可以归一化?