计算机中信息的表示和处理 整数和小数的二进制表示(附Lab)

  • 现代计算机存储和处理的信息以二值信号表示,这些二进制数字称为位,为什么要用二进制来进行编码?因为二进制只有1和0两种状态,正好可以用高低电平来表示,同时可以数值之间可以方便的进行逻辑和算数运算,否则如果用人类容易理解的十进制编码,那机器如何识别这10个状态呢?比较困难
  • 大多数计算机用8位的块(比特),或者说1字节作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存(virtual memory),内存中每一个字节都有一个唯一数字标识,称为它的地址(address)

整数

进制

  • 二进制 ( B i n a r y ) (Binary) (Binary)后缀是 B B B,十进制 ( D e c i m a l ) (Decimal) (Decimal)后缀是 D D D,八进制 ( O c t a l ) (Octal) (Octal)后缀是 O O O,十六进制 ( H e x ) (Hex) (Hex)后缀是 H H H
  • 这里相信读者对任意进制转十进制以及十进制转任意进制(整数和小数部分分开处理然后拼起来)并不陌生,简单说一下二进制和八进制或者十六进制的互相转化,比如下面这个十六进制数
    0 x 39 A 7 F 8 0x39A7F8 0x39A7F8
  • 怎么把它转化成二进制数?最简单的办法就是一位一位转换,比如 3 3 3可以转换成 ( 0011 ) 2 (0011)_2 (0011)2 9 9 9可以转化成 1001 1001 1001这样转化下去,拼起来就可以得到结果如下
    0011   1001   1010   0111   1111   1000 0011\ 1001\ 1010\ 0111\ 1111\ 1000 0011 1001 1010 0111 1111 1000
  • 二进制转十六进制一个道理

  • 在Windows系统中我们也许知道有32位和64位的区别,那么这个区别到底是什么呢?事实上,每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size),字长决定的最重要的系统参数就是虚拟地址空间的访问最大大小,也就是说,对于一个字长为 w w w的机器而言,虚拟地址的范围就是 0 − 2 w − 1 0-2^w-1 02w1,程序最多访问 2 w 2^w 2w个字节,那么32位系统的虚拟地址空间就是 2 32 2^{32} 232个字节,大概是4GB
  • 那么问题来了,跨越多字节的程序对象如何处理呢?答案应该是使用地址来找到这个对象,同时还要知道如何在内存中排列这些字节。因为几乎所有的机器上多字节对象都被存储位连续的字节序列,对象的地址为所使用字节中最小的地址
  • 有两种方法排列这些字节,要么从低到高,称为小端法(little endian),要么从高到低,称为大端法(big endian),这里的高低可以这样理解,比如数字123456,1属于高,6属于低。有时候这会造成问题,比如网络传输数据的时候,不同方法的机器可能导致传输字节反序,这需要注意,我们可以看下面这个例子
#include <bits/stdc++.h>

using namespace std;

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, size_t len){
  size_t i;// unsigned int
  for(int i=0;i<len;i++){
    printf(" %.2x", start[i]);// start是val的首地址,因为两个十六进制位占1个字节,所以输出两位
  }
  printf("\n");
}
int main(){
  int val = 0x87654321;
  byte_pointer valp = (byte_pointer) &val;// 取地址
  show_bytes(valp, 1);// 21
  show_bytes(valp, 2);// 21 43
  show_bytes(valp, 3);// 21 43 65
  return 0;
}
  • 可以测试一下运行结果,小端法机器输出如上图
  • 我们可以用C语言的运算符sizeof来确定对象使用的字节数

移位运算

  • 默认读者使用过移位运算,大概知道怎么回事,C语言移位有左移和右移,左移就是末位补0,高位舍弃;右移有两种,算数右移和逻辑右移,但C语言没有明确规定,逻辑右移是在左端补0,算数右移是在左端补最高位的值,比如现在最高位是1,那就在左端全补1,也就是说算数右移不改变数的正负
  • 而Java中规定明确,x>>k表示算数右移,x>>>k表示逻辑右移

无符号数和有符号数

  • 假设一个整数数据类型有 w w w位,我们可以把位向量写成 x ⃗ \vec{x} x ,表示整个向量,那么整个数字就是 [ x w − 1 , x w − 2 , ⋯   , x 0 ] [x_{w-1},x_{w-2},\cdots,x_{0}] [xw1,xw2,,x0],表示向量中的每一位,所以对于一个无符号数有 B 2 U w ( x ⃗ ) = ∑ i = 0 w − 1 x i 2 i B2U_{w}(\vec{x})=\sum_{i=0}^{w-1}x_i2^i B2Uw(x )=i=0w1xi2i其中 B 2 U w B2U_{w} B2Uw表示 B i n a r y   t o   U n s i g n e d Binary\ to\ Unsigned Binary to Unsigned,长度为 w w w,这个函数是一个双射,意思就是正向一对一,反向也一对一,所以我们可以用每一个数表示一个状态,进行状态压缩
  • 对于有符号数,一般计算机表示方式是补码形式,我们用函数 B 2 T w ( B i n a r y   t o   T w o ′ s − c o m p l e m e n t ) B2T_w(Binary\ to\ Two's-complement) B2Tw(Binary to Twoscomplement)表示,补码编码的定义如下,对向量 x ⃗ = [ x w − 1 , x w − 2 , ⋯   , x 0 \vec{x}=[x_{w-1},x_{w-2},\cdots,x_0 x =[xw1,xw2,,x0],有 B 2 T w ( x ⃗ ) = − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i B2T_w(\vec{x})=-x_{w-1}2^{w-1}+\sum_{i=0}^{w-2}x_i2^i B2Tw(x )=xw12w1+i=0w2xi2i
  • 有符号数的另外两种标准的表示方法是原码反码,原码 ( S i g n − M a g n i t u d e ) (Sign-Magnitude) (SignMagnitude)的最高有效位是符号位,用来确定剩下的位是正权还是负权,有 B 2 S w ( x ⃗ ) = ( − 1 ) x w − 1 × ( ∑ i = 0 w − 2 x i 2 i ) B2S_w(\vec{x})=(-1)^{x_{w-1}}\times (\sum_{i=0}^{w-2}x_i2^i) B2Sw(x )=(1)xw1×(i=0w2xi2i)
  • 反码 ( O n e ′ s   C o m p l e m e n t ) (One's\ Complement) (Ones Complement)表示的话,最高有效位的权比补码多了个1,表示如下 B 2 O w ( x ⃗ ) = − x w − 1 ( 2 w − 1 − 1 ) + ∑ i = 0 w − 2 x i 2 i B2O_w(\vec{x})=-x_{w-1}(2^{w-1}-1)+\sum_{i=0}^{w-2}x_i2^i B2Ow(x )=xw1(2w11)+i=0w2xi2i
  • 但是你会发现原码和反码在表示0的时候都有两种情况,原码是因为最高位表示符号,是不是1对于0无影响,反码是因为最高有效位权多了个1,导致所有位都是1的时候值变成了0
  • 对于很多国内教材,讲到补码的时候会说,对于一个正整数,补码和原码相同(原码就是这个数转化成二进制),对负整数来说,补码是原码取反+1,从上面的公式中可以轻松推出这一结论
  • C语言中的强制类型转换非常简单,就是位表示不会改变,只是改变了解释这些位的方式,那么如果一个有符号的负数转化成一个无符号的数就可能变成一个大正数,如果位不够要进行扩展,扩展可能是零扩展也可能是符号扩展,这个和逻辑右移与算数右移比较类似,如果位太多要截断,这里书中有详细介绍,我就不求甚解了

加法运算

  • 显然由于计算机字长和内存的限制,一个数字不能无限制的增大,否则就会失去它原本的意义,加法实际上就是超过部分舍掉就完事了,两个无符号数加法公式如下 x + w u y = { x + y , x + y < 2 w 正常 x + y − 2 w , 2 w ≤ x + y < 2 w + 1 溢出 x+{^u_w}y=\begin{cases}x+y,\quad &x+y\lt 2^w &正常 \\ x+y-2^w,&2^w\leq x+y\lt2^{w+1} &溢出 \end{cases} x+wuy={x+y,x+y2w,x+y<2w2wx+y<2w+1正常溢出
  • 补码加法同理 x + w t y = { x + y − 2 w , 2 w − 1 ≤ x + y 正溢出 x + y , − 2 w − 1 ≤ x + y < 2 w − 1 正常 x + y + 2 w , x + y < − 2 w − 1 负溢出 x+{^t_w}y=\begin{cases}x+y-2^w,\quad &2^{w-1}\leq x+y &正溢出 \\ x+y,&-2^{w-1}\leq x+y\lt2^{w-1} &正常\\x+y+2^w,&x+y\lt -2^{w-1} &负溢出\end{cases} x+wty= x+y2w,x+y,x+y+2w,2w1x+y2w1x+y<2w1x+y<2w1正溢出正常负溢出
  • 上面的公式简单来说就是,你就直接加,超出位数就不要了,得到的答案就是运算结果

小数

定点表示

  • 小数转化为二进制比较容易,对于整数部分,按照整数转化为二进制的方式进行转化;对于小数部分,通常采用的方法是乘 2 2 2取整,直到符合的位数
  • 这是定点表示的方法,但是对于类似 1 × 2 100 1\times2^{100} 1×2100这样非常巨大的数字,我们不能够使用定点表示的方法,接下来看如何使用浮点表示

IEEE 浮点表示

  • 浮点数对形如 V = x × 2 y V=x\times 2^y V=x×2y的有理数进行编码,换句话说就是将小数转换为二进制表示,然后再对其进行编码,乘 2 y 2^y 2y是什么意思?就是二进制移位

IEEE浮点标准用 V = ( − 1 ) s × M × 2 E V=(-1)^s\times M\times 2^E V=(1)s×M×2E的形式来表示一个数

  • 符号(sign)s决定这个数是正数还是负数,而对于数值0的符号位解释作为特殊情况处理
  • 尾数(significand)M是一个二进制小数,它的范围是 1 ∼ 2 − ε 1\sim2-\varepsilon 12ε,或者是 0 ∼ 1 − ε 0\sim1-\varepsilon 01ε
  • 阶码(exponent)E的作用是对浮点数加权,这个权重是2的E次幂(可能是负数)

将浮点数的位表示划分为三个字段,分别对这些值进行编码,分为单精度(32位的float)和双精度(64位的double)
在这里插入图片描述

  • 根据上图可以解决一个问题,那就是单精度和双精度数能精确到小数点后多少位,根据即将要讲到的规格化定义,尾数表示的是 2 M + 1 2^{M+1} 2M+1,所以显然单精度能表示到 2 24 = 16777216 2^{24}=16777216 224=16777216,这个数在 1 0 7 10^7 107 1 0 8 10^8 108之间,排除掉最后一位的舍入问题,单精度能精确到小数点后 6 6 6位;同理双精度能表示到 2 53 = 9007199254740992 2^{53}=9007199254740992 253=9007199254740992,在 1 0 16 10^{16} 1016 1 0 17 10^{17} 1017之间,所以能精确到小数点后 15 15 15

规格化和非规格化

在这里插入图片描述
下面对上图进行一些解释

  • 根据阶码,引入了规格化和非规格化的概念,如果阶码的所有二进制位不全为0,也不全为1,那么就说这个二进制小数是规格化的,但是在这种情况下,阶码字段被解释成以偏置(biased)形式表示的有符号整数,也就是说,阶码的值是 E = e − B i a s E=e-Bias E=eBias,其中 e e e是无符号数,而 B i a s Bias Bias是一个等于 2 k − 1 − 1 2^{k-1}-1 2k11(单精度127,双精度1023)的偏置值。由此,指数的取值范围对于单精度是 − 126 ∼ + 127 -126\sim+127 126+127,双精度是 − 1022 ∼ + 1023 -1022\sim+1023 1022+1023,小数字段 f r a c frac frac被解释为描述小数值 f f f,这个 f f f的范围是 [ 0 , 1 ) [0,1) [0,1),二进制表示为 0. f n − 1 . . . f 1 f 0 0.f_{n-1}...f_1f_0 0.fn1...f1f0,正常情况下是这样,但是我们可以通过调整阶码 E E E让这个二进制数变成 1. f n − 1 f n − 2 . . . f 0 1.f_{n-1}f_{n-2}...f_0 1.fn1fn2...f0,这样,这个开头的 1 1 1不需要记录,就提高了1位的精度位,这就是规格化表示
  • 对于非规格化,阶码域就全是0,阶码值是 E = 1 − B i a s E=1-Bias E=1Bias,这样设置阶码值的意义是从规格化到非规格化能够平滑转换,而尾数的值是 M = f M=f M=f,也就是小数字段的值,不包括隐含的开头的1,因为规格化数后面的二进制小数是1.几,所以没办法表示0,那么非规格化就可以表示 0.0 0.0 0.0,但是如果符号位是1的话,得到的就是 − 0.0 -0.0 0.0,IEEE浮点格式对这两种数也有它自己的规定,另外一个就是逐渐下溢,可以表示非常接近于0.0的数
  • 当阶码所有位都是1的时候,如果小数域都是0,那么这个数就表示无穷,不都是0就表示NaN(Not a Number),如果有两个非常大的数相乘,那么得到的结果就会是inf,也就是无穷

舍入

  • 因为浮点数只能近似表示实数,所以需要考虑如何进行舍入,一般有四种舍入方式,下面以舍入到整数为例
  1. 向上舍入,很简单,比如1.2,向上舍入就是2,如果是-1.2向上舍入就是-1,就是往大了取
  2. 向下舍入,和第一个相反,1.2向下舍入是1,-1.2向下舍入是-2
  3. 向零舍入,1.2向零舍入就是1,-1.2向零舍入就是-1
  4. 向偶数舍入,这个比较特殊,它将数字向上或者向下舍入,使得最终结果是距离这个数字最近的数;如果距离相等则舍入到有效数字的最后一位是一个偶数的数,比如说1.4会舍入到1,因为1.4处于1和2之间,而它距离1比较近,所以舍入到1;如果是1.5,因为它距离1和2一样近,这个时候因为1是个奇数,所以需要舍入到2,如果是2.5则向下舍入到2。那么为什么要引入这样奇怪的方式呢?主要是避免大量的向上或者向下取整带来的统计偏差,相当于取了一个平均值

浮点运算

  1. 对阶,目的是让两个操作数的小数点位置对齐,使得两个数的阶码相等,这样我们先求阶差,然后以小阶向大阶对齐的原则,具体操作过程就是尾数右移,给阶码腾出空间,这样尾数的低位数就会丢失,这也就说明了为什么要以小阶向大阶对齐,如果返回来那么势必要左移,这样尾数的高位就要丢失,产生错误,因为低位丢失误差较小,高位丢失误差很大
  2. 尾数求和,将对阶之后的尾数按照定点数加(减)运算规则运算。运算后的尾数不一定是规格化的,因此,浮点数的加减运算需要进一步进行规格化处理
  3. 规格化,IEEE 754规格化尾数的形式是 ± 1. \pm1. ±1.x ⋯ \cdots x,但是尾数加减之后不一定能得到这种情况,可能小数点之前好几位,或者小数点前面是0,这时候需要进行规格化处理,具体方法是左规右规,直到最高位1移动到小数点前作为隐藏位,这个在前面的规格化中已经说过
  4. 舍入,因为前面对尾数进行了一些右移操作,可能会造成一些浮点误差,为了保证运算精度,一般将低位移出的两位保留下来,参加中间过程的运算,最后将运算结果进行舍入,还原表示成IEEE 754格式,常见的舍入方法有0舍1入法(舍去的尾数最高位如果是1则对新的尾数+1,否则不动,但是这样可能使尾数溢出,此时需要再做一次右规)、恒置1法(只要因移位而丢失的位中有1就把尾数末位置1)和截断法(恒舍法)(直接截取所需位数,丢弃后面的所有位)
  5. 溢出判断,看指数是不是超过了最大允许值或者低于最小允许值,如果是这样的话就发生了溢出,进行相应的处理

阿贝尔群:又称交换群或加群,它由自身的集合G和二元运算*组成,它除了满足一般的群公理,即运算的结合律、G有单位元、所有G的元素都有逆元之外,还满足交换律公理。因为阿贝尔群的群运算满足交换律和结合律,群元素乘积的值与乘法运算时的次序无关,换句话说对于任意的 a , b ∈ G a,b\in G a,bG,都有 a b = b a ab=ba ab=ba,或者说 a + b = b + a a+b=b+a a+b=b+a

  • 可以看出整数加法形成了一个阿贝尔群,而浮点数加法由于可能溢出到无穷大并不满足结合律所以不是一个阿贝尔群,这个需要注意

Lab

http://csapp.cs.cmu.edu/3e/labs.html

  • 首先进行环境配置,我们用docker来模拟centos环境,如果自己有linux服务器也可(有服务器的同学应该自己能搞定),如果不会docker请先简单学一下 . . . . . . ...... ......
  • 我在Windows下安装一个docker desktop,在控制台输入docker -v,docker版本如下
    在这里插入图片描述
  • 使用docker pull ubuntu拉取最新版本的ubuntu镜像
  • 然后docker run -it -v /C/Users/clarence/Desktop/csapp:/csapp --name=csapp ubuntu /bin/bash,运行ubuntu并挂载指定本地文件夹下,注意路径的写法,分隔符是:,别把我的路径复制进去
  • apt-get update更新源
  • apt-get install build-essential
  • apt-get install gdb
  • apt-get install gcc-multilib
  • 之后我们解压下载好的文件tar xvf datalab-handout.tar,然后会发现一个文件bits.c

1. bitXor(x,y)

用按位与 & \And &和按位取反~求 x ⊕ y x\oplus y xy
Legal ops: ~ &

  • 显然是德摩根律,我们尝试从理性的角度理解一下异或运算和与或非的关系,真值表可以简单写一下就是 ( 0 , 0 ) , ( 0 , 1 ) , ( 1 , 0 ) , ( 1 , 1 ) (0,0),(0,1),(1,0),(1,1) (0,0),(0,1),(1,0),(1,1),相同的值异或之后为0,首先我们可以写一个 x ∨ y x\lor y xy,可以发现只有 ( 1 , 1 ) ≠ 1 ⊕ 1 (1,1)\neq 1\oplus1 (1,1)=11,考虑如何解决它,容易发现如果我们对 x , y x,y x,y全取非,再次求取 x ∨ y x\lor y xy依然不影响 ( 0 , 1 ) (0,1) (0,1) ( 1 , 0 ) (1,0) (1,0)的值,所以可以得到 x ⊕ y = ( x ∨ y ) ∧ ( ¬ x ∨ ¬ y ) x\oplus y=(x\lor y)\land(\neg x\lor\neg y) xy=(xy)(¬x¬y)
  • 再次用德摩根律展开,把或运算变成与运算可以得到 x ⊕ y = ¬ ( ¬ x ∧ ¬ y ) ∧ ¬ ( x ∧ y ) x\oplus y=\neg(\neg x\land\neg y)\land\neg(x\land y) xy=¬(¬x¬y)¬(xy)
int bitXor(int x, int y) {
  return ~((~x) & (~y)) & ~(x & y);
}

2. tmin()

求二进制补码表示下int的最小值
Legal ops: ! ~ & ^ | + << >>

  • 显然符号位为1即可
int tmin(void) {
  return (1 << 31);
}

3. isTmax(x)

x x x是不是 i n t int int最大值,如果是就返回 1 1 1,否则返回 0 0 0
Legal ops: ! ~ & ^ | +

  • 首先我们知道 i n t int int最大值是除了符号位全是1,但是最大的麻烦是不能用等号,那怎么办呢?
  • 这里注意一下取非和取反的区别,C语言中,非0数取非就是0,只有 0 0 0取非才是 1 1 1,根据这个性质,也许我们能够找到一些突破口
  • 如果 x x x满足0111…这种,那么 x ⊕ ( x + 1 ) = − 1 x\oplus(x+1)=-1 x(x+1)=1,所以使用 ¬ ( x ⊕ ( x + 1 ) + 1 ) \neg(x\oplus(x+1)+1) ¬(x(x+1)+1)来判断
  • 但是有一种特殊情况就是 x = − 1 x=-1 x=1,需要用 b o o l bool bool类型特判掉,所以就是 ¬ ¬ ( x + 1 ) \neg\neg(x+1) ¬¬(x+1),两种情况取与运算即可
int isTmax(int x) {
  return !((x ^ (x + 1)) + 1) & !!(x + 1);
}

4. allOddBits(x)

如果所有奇数位都是1就返回1,否则返回0
Legal ops: ! ~ & ^ | + << >>

  • 不要陷入思维定势…我们不仅可以和 x x x取与运算,还可以和任意的数字取与运算,所以显然找一个奇数位全是1的数(0xAAAAAAAA)与一下就好了
  • 注意奇数位从高往低数,最高位是第 1 1 1
int allOddBits(int x) {
  return !((~(x & 0xAAAAAAAA) ^ (0xAAAAAAAA)) + 1);
}

5. negate(x)

返回 − x -x x
Legal ops: ! ~ & ^ | + << >>

  • x~x之间的关系是加和等于 1111...... 1111...... 1111......,这个数再加 1 1 1就是 0 0 0,也就是说 x + ∼ x + 1 = 0 x+\thicksim x+1=0 x+x+1=0,即 − x = ∼ x + 1 -x=\thicksim x+1 x=x+1
int negate(int x) {
  return ~x + 1;
}

6. isAsciDigit(x)

判断 x x x是不是Ascii值在['0','9']的数字
Legal ops: ! ~ & ^ | + << >>

  • 这几个数字的范围是0x30 ∼ \sim 0x39,容易发现它们二进制开头都是0011,剩下的可以分为两类,分别是0...1...,其中1...只有三个数字,我的想法是挨个特判
int isAsciiDigit(int x) {
  return !((~(x >> 4) ^ 3) + 1) & (!((x >> 3) & 1) | !((~(x & 15) ^ 7) + 1) | !((~(x & 15) ^ 8) + 1) | !((~(x & 15) ^ 9) + 1));
}

7. conditional

x ? y : z
Legal ops: ! ~ & ^ | + << >>

  • 问题的关键是如何做出条件筛选效果,容易想到我们需要一个或运算,前面是 y y y,后面是 z z z,如果 x x x不是0,那么让后面的 z z z变成0,前面的 y y y还是 y y y;反之让 y y y变0,后面的 z z z还是 z z z,想起到这个效果,首先将 x x x统一,显然!!x能让 x x x要么是0,要么是1,如果 x x x是0,那么!!x也是0,同理 x x x不是0的话,!!x就是1
  • 现在我们令x=!!x,当 x = 0 x=0 x=0的时候,结果是 z z z,所以有~x&z或者(x-1)&z,又因为 x = 1 x=1 x=1的时候,这一项应该是 0 0 0,所以选择(x-1)&z
  • 现在开始考虑含 y y y的项,容易想到应该是(~x+1)&y,满足所有情况,因为~0+1=0
  • 所以答案应该是x=!!x;((~x+1)&y) | ((x-1)&z)
int Conditional(int x, int y, int z) {
  x = !!x;
  return ((~x + 1) & y) | ((x - 1) & z);
}

8. isLessOrEqual(x, y)

True if x ≤ y, false otherwise
Legal ops: ! ~ & ^ | + << >>

  • 容易想到, x ≤ y    ⟺    x − y ≤ 0    ⟺    x + ∼ y + 1 ≤ 0 x\leq y\iff x-y\leq0\iff x+\sim y+1\leq 0 xyxy0x+y+10
  • x = y x=y x=y的时候异或判断即可
  • 但是这里面有巨大的问题,一个是溢出问题,设 k = ∼ y + 1 k=\sim y+1 k=∼y+1,我们需要特判符号位是1的情况,因为两个负数加起来一定还是负数,两个正数加起来不可能是负数;另外一个问题就是 t m i n tmin tmin,这个数很特殊因为由于正数负数不对称, ∼ y + 1 \sim y+1 y+1还是 y y y本身,所以需要把这个特判掉,同时注意不能把0判掉了,因为0也满足这个性质,所以我想到的方法就是(!!(y ^ k) | !y),把0包进来,然后看 y y y k k k是否相等
int isLessOrEqual(int x, int y) {
  int k = ~y + 1;
  int tx = ((x >> 31) & 1);
  int tk = ((k >> 31) & 1);
  return (!(x ^ y)) | ((!!(y ^ k) | !y) & ((tx & tk) | ((tx ^ tk) & (((x + k) >> 31) & 1))));
}

9. logicalNeg(x))

用其他运算符表示非运算
Legal ops: ~ & ^ | + << >>

  • 确实没想到,这个题很巧妙,我们知道对于 ¬ x \neg x ¬x来说,如果 x = 0 x=0 x=0,那么返回1,其他都是0,数字有正数负数和0,当然 t m i n tmin tmin需要格外关注,那么对于 ∼ x + 1 \sim x+1 x+1,它的符号位正常情况下除了 t m i n tmin tmin和0,应该和 x x x的符号位相反,对于 t m i n tmin tmin,这两个数都是1,对于0,这两个数都是0,所以想到思路,应该把 x x x ∼ x + 1 \sim x+1 x+1取个或运算,然后取最高位,只有0才是0,其他的都应该是1
  • 但是这样还是不行,因为要求如果是0应该返回1,我们现在返回的是0,这个时候要用到算术右移,我们把 x x x ∼ x + 1 \sim x+1 x+1取个或运算之后,往右移31位,得到-1,如果是0的话应该得到0,这样我们再+1就好了
  • 实在巧妙
int logicalNeg(int x) {
  return ((x | (~x + 1)) >> 31) + 1;
}

10. howManyBits(x)

问所给的数用二进制补码表示最少需要多少位
Legal ops: ! ~ & ^ | + << >>

  • 先讲一下例子,比如说12,我们可以用二进制补码01100来表示,最高位是符号位,所以说返回5
  • 对于一个正数,因为最高位是符号位必须是0,所以这一位不能省,然后补码表示只需要找最高的是1的位置就好了,这个比较显然;而负数,最高位是符号位1,同样也不能省,它需要找最高位是0的位置,这是因为对于1来说,会消除掉前一位相邻的1的影响
  • 有了这个基本思路之后,为了统一两种情况,我们需要把负数化正(或者反过来),这个操作的想法和上面的题目是类似的,首先肯定是一个或运算,然后根据符号位来区分或运算的左面是0还是右面是0,具体操作如下
 int k = x >> 31;
 x = (k & ~x) | (~k & x);
  • 接下来我们找1的位置,可以用32行代码来写,但是简单的方法还是二分,先看开头的16位有没有1,如果有1,我们看这16位的开头8位有没有1,依此类推…最后把所有的位加起来再加一个符号位
int howManyBits(int x) {
  int k = x >> 31;
  x = (k & ~x) | (~k & x);
  int x16 = (!!(x >> 16)) << 4;
  x >>= x16;
  int x8 = (!!(x >> 8)) << 3;
  x >>= x8;
  int x4 = (!!(x >> 4)) << 2;
  x >>= x4;
  int x2 = (!!(x >> 2)) << 1;
  x >>= x2;
  int x1 = (!!(x >> 1));
  x >>= x1;
  int x0 = (!!(x >> 0));
  return 1 + x0 + x1 + x2 + x4 + x8 + x16;
}

11. floatScale2(uf)

求浮点数表示下的 2 × u f 2\times uf 2×uf,结果用无符号数表示
Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while

  • 根据IEEE标准, V = ( − 1 ) s × M × 2 E V=(-1)^s\times M\times 2^E V=(1)s×M×2E和浮点数标准, s s s占1位, E E E占8位, M M M占23位,首先可以先把 s , m , e s,m,e s,m,e都提取出来
  • 然后复习一下规格化和非规格化表示方法,如果阶码 E E E全是1,表示无穷大或者NaN;如果阶码 E E E全是0表示非规格化数;其余表示规格化,所以根据题目要求,如果 u f uf uf的阶码全是1,则返回它本身
  • 对于非规格化数,由于阶码为0,只需要对尾数乘2即可
  • 对于规格化数,阶码向左移动一位
unsigned floatScale2(unsigned uf) {
  unsigned s = (uf >> 31) & 1;
  unsigned e = (uf >> 23) & 0xff;
  unsigned m = uf & 0x7fffff;
  if(e == 0xff) return uf;
  else if(!e) return (s << 31) | (e << 23) | (m << 1);
  e += 1;
  return (s << 31) | (e << 23) | m;
}

12. floatFloat2Int(uf)

将浮点数uf转换为整数,Nan或者inf返回0x80000000u
Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while

  • 首先计算出符号位 s s s、阶码 e e e和尾数 m m m
  • 对于规格化数,阶码一共8位,所以 B i a s = 2 7 − 1 = 127 , E = e − B i a s = e − 127 Bias=2^7-1=127,E=e-Bias=e-127 Bias=271=127,E=eBias=e127,尾数的开头是1
  • V = ( − 1 ) s × M × 2 E V=(-1)^s\times M\times2^E V=(1)s×M×2E,所以当 E ≥ 31 E\geq31 E31的时候是溢出的inf,当 e = 255 , E = 128 e=255,E=128 e=255,E=128的时候是 N a n Nan Nan,所以 E ≥ 31 E\geq31 E31返回0x80000000u
  • 如果 E < 0 E\lt0 E<0,值 < 1 \lt1 <1,这个时候返回 0 0 0,对于非规格化数也返回 0 0 0
  • 接下来考虑规格化值的阶码 E E E,如果 E < 23 E\lt23 E<23,为了让阶码恢复到 23 23 23,需要把尾数右移 23 − E 23-E 23E位;同理如果 E > 23 E\gt23 E>23,需要把尾数左移 E − 23 E-23 E23
  • 最后计算舍入之后的整数的时候需要考虑符号位,如果原来的小数是负数需要变尾数为它的相反数
int floatFloat2Int(unsigned uf) {
  int s = (uf >> 31) & 1;
  int e = (uf >> 23) & 0xff;
  int m = uf & 0x7fffff;
  int Bias = 127;
  int E = e - Bias;
  m |= (1 << 23);
  if(E < 0) return 0;
  if(E >= 31) return 0x80000000u;
  if(E < 23) {
    m >>= (23 - E);
  }else {
    m <<= (E - 23);
  }
  if(s) {
    return ~m + 1;
  }
  return m;
}

13. floatPower2(x)

2. 0 x 2.0^x 2.0x
Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while

  • 因为浮点数有一定范围,求的是以 2.0 2.0 2.0为底,当数字是规格化值的时候,根据 E = e − B i a s = e − 127 , 1 ≤ e ≤ 254 E=e-Bias=e-127,1\leq e\leq254 E=eBias=e127,1e254(因为 e ≠ 0 & e ≠ 255 e\neq 0\And e\neq255 e=0&e=255),所以 − 126 ≤ E ≤ 127 -126\leq E\leq 127 126E127,根据 M m i n = 1.0000..... = 1 , M m a x = 1.1111..... = 1 + 2 − 23 M_{min}=1.0000.....=1,M_{max}=1.1111.....=1+2^{-23} Mmin=1.0000.....=1,Mmax=1.1111.....=1+223,得到取值范围为 [ 2 − 126 , 2 127 × ( 1 + 2 − 23 ) ] [2^{-126},2^{127}\times(1+2^{-23})] [2126,2127×(1+223)]
  • 当数字是非规格化值的时候,因为 E = 1 − B i a s = − 126 E=1-Bias=-126 E=1Bias=126 M m i n = 2 − 23 M_{min}=2^{-23} Mmin=223,所以数字最小是 2 − 149 2^{-149} 2149 M m a x = 1 − 2 − 23 M_{max}=1-2^{-23} Mmax=1223(所有位都是1),得到数字最大是 2 − 126 × ( 1 − 2 − 23 ) 2^{-126}\times(1-2^{-23}) 2126×(1223),这个数是在规格化值最值中间的,所以规格化值和非规格化值可以在 − 126 -126 126处分割
  • 对于非规格化值,由于阶码全是0,需要调整尾码,因为 2 x = M × 2 − 126    ⟺    M = 2 x + 126 2^x=M\times2^{-126}\iff M=2^{x+126} 2x=M×2126M=2x+126,设1向左移动 n n n位,有 x + 126 = − ( 23 − n ) , n = x + 149 x+126=-(23-n),n=x+149 x+126=(23n),n=x+149
  • 对于规格化值,只需要调整阶码, x = e − B i a s    ⟺    e = x + B i a s = x + 127 x=e-Bias\iff e=x+Bias=x+127 x=eBiase=x+Bias=x+127
  • 这个题感觉涉及到的点很多
unsigned floatPower2(int x) {
  if(x > 127) return 0xff << 23;
  if(x >= -126) return (x + 127) << 23;
  if(x >= -149) return 1 << (x + 149);
  return 0;
}

全做完之后就可以运行了,首先在目录下make,然后./btest

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Clarence Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值