计算机系统实验二APP Data Lab
实验题目:APP Data Lab
实验目的:我的目标是修改bits.c文档,完成所有函数的编写,利用dlc和btest两个工具通过所有的测试
实验环境:Ubuntu12.04环境
实验内容及操作步骤:
1.bitAnd函数
a.函数要求
- bitAnd - x&y using only ~ and |
- Example: bitAnd(6, 5) = 4
- Legal ops: ~ |
- Max ops: 8
- Rating: 1
b.函数功能
能够实现并返回两个数与操作之后的结果
c.设计思想
由于能够使用的运算符只有~ |,所以我们不能够直接使用与操作符。联系已学知识,可以使用德摩根定律,将x&y转化为((x)|(~y))
d.代码实现
int bitAnd(int x, int y) {
return ~((~x)|(~y));
}
2.getByte函数
a.函数要求
- getByte - Extract byte n from word x
- Bytes numbered from 0 (LSB) to 3 (MSB)
- Examples: getByte(0x12345678,1) = 0x56
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 6
- Rating: 2
b.函数功能
取出一个int数据指定的字节对应的数并以16进制表示出来
c.设计思想
题目的要求就是提取处x的第n个字节,对于以上的例子,78第0字节,56第一字节,以此类推即可,所以这里提出的是0x56。那么这里的思路就是能够让x向又移动8*n个bits即可,单位我们需要提取
转换后的低八位,那么可以用他和(0…0)11111111进行与操作,因为1&x=x,因此得到的就是对应的位
d.代码实现
int getByte(int x, int n) {
n = n << 3;
x = x >> n;
x = x & 255;
return x;
}
3.logicalShift函数
a.函数要求
- logicalShift - shift x to the right by n, using a logical shift
- Can assume that 0 <= n <= 31
- Examples: logicalShift(0x87654321,4) = 0x08765432
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 20
- Rating: 3
b.函数功能
能够正确的实现逻辑位移
c.设计思想
在学习的过程中,我们接触过两种不同的位移方式,一种是算数位移,一种是逻辑位移,这两种在左移的情况下没有什么区别,但在右移的时候逻辑位移都是用0来补充,但算数位移是需要考虑符号位的。所以思路上要考虑将逻辑位移后的前n为进行置零操作,其余保持不变,那么我们可以将位移后的结果和0x(00…0)11…1进行与操作,其中恰好有n个0。那么现在主要的问题就是如何得到0x(00…0)11…1所以为了得到(00…0)11…1可以先(11…1)00…0然后取非,(11…1)00…0可以由100…0进行右移n位得到。
d.代码实现
int logicalShift(int x, int n) {
int temp = ((1 << 31) >> n) << 1;//由于不能使用减号
temp = ~temp;
return (temp & (x >> n));
}
4.bitCount函数
a.函数要求
- bitCount - returns count of number of 1’s in word
- Examples: bitCount(5) = 2, bitCount(7) = 3
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 40
- Rating: 4
b.函数功能
计算一个二进制数中的1的个数
c.设计思想
基于数字电路的设计思想或者迭代思想,即构建一个“具有多层次结构的加法器”,实质上就是先统计一位上的1的个数,然后将结果相加,这样我们就得到两位的统计结果,接着将两位的统计结果相加,那样就可以得到四位的统计结果…由此迭代下去最终的课都达到统计32位1的个数的效果。以下是一个具体的例子:
所以现在需要解决的问题是:怎样去统计结果呢?事实上,我们可以利用掩码,比如有掩码01010101,那么与一字节相与,那么就可以统计偶数位上的1的个数,此时如果我们将原数右移一位,这样就可以达到将刚才的奇数位移动到偶数位,继续同样操作,就达到统计奇数位的效果,最后将这个结果累加…同理之后也是如此,但掩码变成00110011,00001111,其实都是一样的。
d.代码实现
int bitCount(int x) {
int count;
int tmpMask1 = (0x55)|(0x55<<8);
int mask1 = (tmpMask1)|(tmpMask1<<16); //得到掩码: 01010101……01010101
int tmpMask2 = (0x33)|(0x33<<8);
int mask2 = (tmpMask2)|(tmpMask2<<16); //得到掩码: 00110011……00110011
int tmpMask3 = (0x0f)|(0x0f<<8);
int mask3 = (tmpMask3)|(tmpMask3<<16); //得到掩码: 00001111……00001111
int mask4 = (0xff)|(0xff<<16); //得到掩码: 0000 0000 1111 1111 0000 0000 1111 1111
int mask5 = (0xff)|(0xff<<8); //得到掩码: 0000 0000 0000 0000 1111 1111 1111 1111
count = (x&mask1)+((x>>1)&mask1); //分别计算每组2位中,低位的1的个数;再移位求每组2位中,高位的1的个数;求和
count = (count&mask2)+((count>>2)&mask2); //两两相加
count = (count + (count >> 4)) & mask3; //同理,两两相加
count = (count + (count >> 8)) & mask4; //同理,两两相加
count = (count + (count >> 16)) & mask5; //同理,两两相加
return count;
}
5.bang函数
a.函数要求
- bang - Compute !x without using !
- Examples: bang(3) = 0, bang(0) = 1
- Legal ops: ~ & ^ | + << >>
- Max ops: 12
- Rating: 4
b.函数功能
逻辑非操作
c.设计思路
只有当x=0时,x为全1,x+1为全0(逸出),x|(~x+1)的最高位为0。只要x不为0,x与 ~x+1的最高位一定有一个为1,话句话说其实就是除了0以外的所有数,它和它的相反数的符号位必然是不一样的。
d.代码实现
int bang(int x) {
int temp = x | (~x + 1);
temp = ~(temp >> 31);
return (temp & 1);
}
6.tmin函数
a.函数要求
- tmin - return minimum two’s complement integer
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 4
- Rating: 1
b.函数功能
返回二进制最小整形数据
c.设计思路
了解二进制补码的实质和二进制补码的计算规则,在了解二进制补码计算规则的基础上,我们可以知道最小的二进制补码其实就是10…0,所以我们只需要将1进行左移就可以实现这个函数的功能
d.代码实现
int tmin(void) {
int result = 1<<31;
return result;
}
7.fitsBits函数
a.函数要求
- fitsBits - return 1 if x can be represented as an
- n-bit, two’s complement integer.
- 1 <= n <= 32
- Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 15
- Rating: 2
b.函数功能
判断x能否由n位二进制补码表示
c.设计思路
判断x能否由n位二进制补码表示,首先这个数可以被32位表示,将一个数左移32-n然后右移动32-n,可以知道,这个n位实际上是被保留下来的,在移位过程中实际上是把高于n的位全部覆盖了,因此会导致得到的值和之前的不同,所以我们只需要把变换后的数和原始数据进行异或,观察返回值是不是全1即可,或者也可以先取反,观察结果是否为0
d.代码实现
int fitsBits(int x, int n) {
int shiftnumber=32+~n+1;//32-n
int xx=x<<shiftnumber>>shiftnumber;
return !(xx^x);//和原来相同返回1,表示能用n位表示
}
8.divpwr2函数
a.函数要求
- divpwr2 - Compute x/(2^n), for 0 <= n <= 30
- Round toward zero
- Examples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 15
- Rating: 2
b.函数功能
计算x/(2^n)的值并且向0取整
c.设计思想
我们最先想到的思路其实就是移位,这种除法只需要将x向右移位n位即可。但是,由于题目是要求需要向0取整,其实这个就是类似于二进制补码的除法。通过对正常右移的结果和向0取整结果在数轴表示出来,可以明显发现,在正数时结果是相同的,但到了负数的区间,两者之间就有一个明显的且一致的偏置值,这个值就是2^n-1,所以依照这个思想,我们只需要在右移时,加上这个偏置值即可
d.代码实现
int divpwr2(int x, int n) {
int sign = x >> 31;
int mask = (1 << n) + (~0);
return (x + (sign & mask)) >> n;
}
9.negate函数
a.函数要求
- negate - return -x
- Example: negate(1) = -1.
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 5
- Rating: 2
b.函数功能
计算相反数
c.设计思路
补码操作,取反加一
d.代码实现
int negate(int x) {
return (~x) + 1;
}
10.isPositive函数
a.函数要求
- isPositive - return 1 if x > 0, return 0 otherwise
- Example: isPositive(-1) = 0.
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 8
- Rating: 3
b.函数功能
判断一个数是否是正数返回1,否则返回0
c.设计思路
一个整数无非就三种情况,大于0,等于0,小于0。对于正数和负数,显然我们只需要观察它们的符号位即可,而对于等于0的数,就只需要再加一个另外的判断条件即可
d.代码实现
int isPositive(int x) {
return !((x>>31)|(!x));
}
11.isLessOrEqual函数
a.函数要求
- isLessOrEqual - if x <= y then return 1, else return 0
- Example: isLessOrEqual(4,5) = 1.
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 24
- Rating: 3
b.函数功能
判断x<=y,是则返回1否则返回0
c.设计思路
首先想到的是可不可以从高位直接一位一位的进行比较,答案是太麻烦了。通过转换,其实我们可以观察x-y的结果来判断进行输出。如果结果的符号位是1或者结果是0,那么显然x<=y,这个时候输出1。但真是这样吗?并不是,因为在计算机中会出现一个非常严重的问题,就是溢出,对于这里也一样会出现。对于加法同号是会溢出的,减法相反异号会溢出。因此同号的时候只需要观察x-y的符号位,如果异号,就意味着x是正数y为负数,其实只需要输出x的符号位即可
d.代码实现
int isLessOrEqual(int x, int y) {
int signx=(x>>31)&1;//x的符号位
int signy=(y>>31)&1;//y的符号位
int signy_x=((y+~x+1)>>31)&1;//y-x的符号位
return (!(signx^signy)&!signy_x) | ((signx^signy)&signx);//同号 异号
}
12.ilog函数
a.函数要求
- ilog2 - return floor(log base 2 of x), where x > 0
- Example: ilog2(16) = 4
- Legal ops: ! ~ & ^ | + << >>
- Max ops: 90
- Rating: 4
b.函数功能
计算x对2的对数并且向下取整
c.设计思路
返回log(x),这个返回的其实是x对2的对数并且向下取整,这也就意味着,只关注最高位的一个1即可。那么关键问题就是如何得到这个最高位1的位置,换个角度讲,我们可以统计最高位1前面0的个数,因为最高位之前所有的位必定位0(因为log不会出现负数)。因此思路如下,首先看前16位是不是全为0,不是则看前16位前8位,是则看后16位的前8位…以此类推
d.代码实现
int ilog2(int x) {
int logs=(!!(x>>16))<<4;
logs=logs+((!!(x>>(logs+8)))<<3);
logs=logs+((!!(x>>(logs+4)))<<2);
logs=logs+((!!(x>>(logs+2)))<<1);
logs=logs+(!!(x>>(logs+1)));
return logs;
}
13.float_neg函数
a.函数要求
- float_neg - Return bit-level equivalent of expression -f for
- floating point argument f.
- Both the argument and result are passed as unsigned int’s, but
- they are to be interpreted as the bit-level representations of
- single-precision floating point values.
- When argument is NaN, return argument.
- Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
- Max ops: 10
- Rating: 2
b.函数功能
返回一个unsigned表示浮点数的负数
c.设计思路
功能比较简单,只需要把符号位给取一个反,可以用异或实现。但要注意的是,对于nan我们需要直接进行输出,不然会出现错误。
d.代码实现
unsigned float_neg(unsigned uf) {
unsigned int a=0xff;
unsigned int b=1<<31;
unsigned int exp=(uf>>23)&a;//取出阶码
unsigned int frac=uf&0x7fffff;//尾数
if(!(exp^a)&&frac) return uf;//当nan,返回uf否则返回-uf
return uf^b;
}
14.float_i2f函数
a.函数要求
- float_i2f - Return bit-level equivalent of expression (float) x
- Result is returned as unsigned int, but
- it is to be interpreted as the bit-level representation of a
- single-precision floating point values.
- Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
- Max ops: 30
- Rating: 4
b.函数功能
返回一个int数据强制转换位浮点数的unsigned类型表示
c.设计思路
浮点数由三个部分组成,按无符号正数相加即可。按照《深入了解计算机系统》课本中,我们首先要得到这个int数的位数,同时得到符号位,阶码和尾数,对于负数,需要正数化,对于阶码要判断是否是nan。其中最重要的就是尾数,它可能会存在一个截尾,这就需要四舍六入五向偶。具体操作就是最低位下一位加一个1,试想一下,如果最低位下一位如果为0,该操作无进位,最后我们进行一个减1操作,为的就是区分五向偶(进位后需要判断和处理变成nan的情况)。
d.代码实现
unsigned float_i2f(int x) {
if(!x) return 0;//为0返回
else{
int exp=0;
unsigned temp1=x;
unsigned temp2,temp3;
int shift;
int sign=0x80000000&x;//取出符号位
if (sign) temp1 = ~temp1 + 1;//如果是负数取它的相反数
temp3=temp2=temp1;
//以下是计算x的位数
while(temp1){
exp=exp+1;
temp1=temp1>>1;
}
exp = exp + ~1 + 1;// 减一操作,由于exp得到的是x的位数,但是在转换过程中我们取得是位数,举个例子:10011011->1.0011011*2^7,我们要尾数的位数即exp-1
shift=23-exp;//计算要移位的长度
if(shift>=0) temp3=temp3<<shift;//shift为正,表示可以用23位表示
else//否则发生截取,我们需要右移,并且要考虑舍入
{
shift=-shift;
temp2=temp2>>shift;//右移shift位
temp3+=(1<<(shift-1));//无论怎样,我们都加上低位的进位
if(!(temp2&1)) temp3+=-1;//如果最低位不为1,说明无法发生偶数进位,所以减去1。如果四舍五入,则不会借位成功舍入,否则不发生进位。试着带入一个例子:对于(1.001)10,(1.001)11,(1.010)11,(1.010)10,(1.001)01
temp3=temp3>>shift;//右移shift
}
exp=((exp+0x7f)<<23)+(temp3>>1);//加上temp3可能发生向exp的进位
exp=exp&0xff800000; //清零frac
temp3=temp3&0x7fffff;//取后23位
return sign|exp|temp3;
}//合并
}
15.float_twice函数
a.函数要求
- float_twice - Return bit-level equivalent of expression 2*f for
- floating point argument f.
- Both the argument and result are passed as unsigned int’s, but
- they are to be interpreted as the bit-level representation of
- single-precision floating point values.
- When argument is NaN, return argument
- Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
- Max ops: 30
- Rating: 4
b.函数功能
返回一个unsigned表示的浮点数2的结果
c.设计思路
对于一个浮点数2的结果,需要对非规格化数和规格化数进行区分,其中非规格化数是左移,规格化是直接阶码+1(要判断是否会变成nan,如果会进一步处理)。为什么非规格化数是直接移位了,对于非规格化数它×2以后很有可能还是非规格化数,而非规格化数的阶码一定是八个0,意味着我们只能对尾数移位,不然会把一个非规格化数转化为规格化数(数值也不一样了),除非一个规格化数×2之后超过非规格化数表示范围,也就是产生了进位,这时候阶码加一,对于规格化数的×2就直接改阶码,但也要注意是否会让规格化数变成一个nan
d.代码实现
unsigned float_twice(unsigned uf) {
unsigned int s=uf&0x80000000;
unsigned int exp=uf&0x7f800000;
unsigned int frac=uf&0x7fffff;
if(exp==0x7f800000)//Nan and inf
{
return uf;
}
else if(!exp)//非规格化数
{
frac<<=1;
}
else//规格化数,直接exp+1,同时如果exp全1那么需要对frac清零防止nan
{
exp+=(1<<23);
if(exp==0x7f800000) frac=0;
}
return (s|exp|frac);
}
实验结果及分析:
实验结果:
1.输入命令 ./dlc bits.c以及./dlc -e bits.c查看语法错误并检查操作符消耗
2.编译并执行程序,输入命令make btest以及./btest执行
结果分析:
经过第一步的操作之后,我们可以看到没一个函数使用的操作符资源消耗,它们分别为4,3,6,33,6,1,7,7,2,4,16,27,8,30,11,可以判断都满足试验的要求。之后通过第二步的操作,可以观察到各项得分分别是1,2,3,4,4,1,2,2,2,3,3,4,2,4,4,最后总得分是41,程序都满足实验要求。
收获与体会:
这次实验真是让我感到了计算机系统下的支配,对于有些题目自己有思路,但往往是不能够在要求内给实现的,不能用if不能用while,又不能用其他啥啥的,反正就整的相当难受,一道题能够卡那里半天,有许多的题目承认自己是通过搜索资料整理的,之前是直接莽代码,思路都没看,结果发现肝不动。这15道题,做了两遍,第一遍算是照个面吧,看了看其他人的代码和思路,马马虎虎的把代码给“模仿了一遍”。但我知道这肯定是不够的,自己必然没有理解,于是自己手写,真的手写把所有题目的思考过程重新一遍手写了一遍,于是就把所有题目给肝透了。从初次接触到完全理解确实花了不少功夫,在其中是深刻意识到计算机的一些基础操作比如异或,与,或等等操作符的实用性,之前也很少用,并且其中有个非常重要的问题,就是所有的操作基本上都是通过位来实现的,移位实现除法,掩码辅助来实现计算等等都非常实用。
几个值得思考的问题?我们是否能够通过这次实验真正了解计算机内部的工作机制,在前一个实验我们了解了汇编,这次实验我们用位操作来实现一些基础功能,所以我们是否能够建立起联系?可以看到,其中有些函数是用到了许多的操作符的,我们能否进行优化,进一步减少操作符的使用,实质上就是有没有更好的算法?最后就是问问自己,是否真正的理解了非规格化数和规格化数以及它们之间的区别,这很重要!
https://blog.csdn.net/weixin_44307065/article/details/105497452
https://blog.csdn.net/qq_40889820/article/details/88831962