参考
- CSapp-DataLab——深入理解函数实现原理: https://blog.csdn.net/weixin_50295745/article/details/127150383
- CSAPP 之 DataLab详解,没有比这更详细的了: https://zhuanlan.zhihu.com/p/59534845
一、实验目的
本实验目的是加强学生对位级运算的理解及熟练使用的能力。
二、报告要求
本报告要求学生把实验中实现的所有函数逐一进行分析说明,写出实现的依据,也就是推理过程,可以是一个简单的数学证明,也可以是代码分析,根据实现中你的想法不同而异。
三、函数分析
表一
bitXor函数
函数要求:
函数名 | bitXor |
---|---|
参数 | int , int |
功能实现 | x^y |
要求 | 只能使用 ~ 和 & |
实现分析:
x^y = (~x&y)|(x&~y)
由于只能使用 ~
和 &
,所以
x^y = ~(~(x^y))
= ~(~((~x&y)|(x&~y)))
= ~(~(~x&y)&(~(x&~y)))
函数实现:
int bitXor(int x, int y) {
return ~(~(~x&y)&~(x&~y));
}
getByte函数
函数要求:
函数名 | getByte |
---|---|
参数 | int , int |
功能实现 | 从字x中取出第n个字节 |
要求 | ! ~ & ^ | + << >> |
实现分析:
已知按位与中1
和任何数相与(1或0)等于这个数本身,0
和任何数相与(1或0)等与0,利用这个原理,可以将给定字x
右移适当位数后与0xff
相与,从而提取出指定字节。
比如 x = 0x12345678
和 n=1
,
y = x >> (1<<n) = x >> 2 = 0x??123456
;
Byte_n = y&0xff = 0x56
函数实现:
int getByte(int x, int n) {
return (x>>(n<<3))&0xFF;
}
logicalShift函数
函数要求:
函数名 | logicalShift |
---|---|
参数 | int , int |
功能实现 | 逻辑右移 |
要求 | ! ~ & ^ | + << >> |
实现分析:
对于x实现逻辑右移n位,
可以首先对x
执行x>>n
得到算术右移n
位后的结果x'
,
由于逻辑右移最高位补零,因此可以通过将算术右移的结果x'
在移位过程中产生的高位全部清零得到逻辑右移的结果。这个操作可以通过掩码实现。
即对于 x=0x12345678
,逻辑右移n=8
位,
可以首先得到x的算术右移结果 y = x>>n = 0x??123456
,
然后与mask = 0x00ffffff
相与,得到结果 0x00123456
。
那么接下来要解决的就是mask
如何得到的问题。
实际上可以通过 ((~(1<<31)>>n)<<1)+1
得到mask
。
函数实现:
int logicalShift(int x, int n) {
int mask;
x = x>>n;
mask = ~(1<<31);
mask = ((mask>>n)<<1)+1;
x = x&mask;
return x;
}
bitCount函数
函数要求:
函数名 | bitCount |
---|---|
参数 | int |
功能实现 | 计算x中1的数目 |
要求 | ! ~ & ^ | + << >> |
实现分析:
-
首先这样得到了一个32位的掩码
mask
,其中每个16位块中的每个字节都是1
。 -
接下来,通过多次按位与运算和右移操作,将整数
x
中每个16位块中的1的个数累加到变量s
中。 -
随后,将
s
右移16位,并与原来的s
相加,这是为了将高16位和低16位的计数相加到一起。 -
然后,创建新的掩码
mask
为0x0F0F
,这个新的掩码用来将s
中每个字节中(每8位)的1的个数相加到一起(使用与上一步骤不同的x0F0F
形式的掩码是考虑到在进行相加求和的同时,和的上限16需要5位表示)。 -
最后,将
s
与mask
进行按位与运算,然后将结果与s
右移4位后与mask
进行按位与运算的结果相加。这一步是为了将s
中每个字节中的1的个数相加到一起。 -
最后一步,将
s
与s
右移8位后的结果相加(计算得到全部位的和),并将最终结果与0x3F
进行按位与运算(32位全1求和结果为32,因此最多需要6位数表示,可通过0x3F
的掩码对高位清零,得到最终结果),最终返回计算出的1的个数。
函数实现:
int bitCount(int x) {
int m1 = 0x11|(0x11<<8);
int mask = m1|(m1<<16);
int s = x&mask;
s += (x>>1)&mask;
s += (x>>2)&mask;
s += (x>>3)&mask;
s += s>>16;
mask = 0xF|(0xF<<8);
s = (s&mask) + ((s>>4)&mask);
return (s+(s>>8))&0x3F;
}
conditional函数
函数要求:
函数名 | conditional |
---|---|
参数 | int , int, int |
功能实现 | 实现c语言中的三目运算 x ? y : z |
要求 | ! ~ & ^ | + << >> |
实现分析:
根据三目运算的逻辑,x
为真则返回y
,x
为假则返回z
;
也即条件为 x
的真假,那么 我们可以通过表达式 !!(x)
判断x
的真假,
因此我们可以通过表达式 condition = ~(!!x) + 1
来表达条件,当x
为真(即x
不为0)时,condition=0xffffffff
,当x
为假(x=0)时condition=0
;
于是最终可以通过(condition&y)|(~condition&z)
实现三目运算 x ? y : z
。
函数实现:
int conditional(int x, int y, int z) {
int condition =~(!!x)+1;
return (condition&y)|(~condition&z);
}
表二
tmin函数
函数要求:
函数名 | tmin |
---|---|
参数 | void |
功能实现 | 返回最小的补码 |
要求 | ! ~ & ^ | + << >> |
实现分析:
在32位系统int
类型的变量中,最小的补码就是 0x80000000
(-2**(31)
),可通过1<<31
得到。
函数实现:
int tmin(void) {
return 1<<31;
}
fitsBits函数
函数要求:
函数名 | fitsBits |
---|---|
参数 | int , int |
功能实现 | x的补码是否可以表示成n位 |
要求 | ! ~ & ^ | + << >> |
实现分析:
若 x
左移32-n
位后,再右移32-n
位,仍与原来相同,则 x
补码可以表示成 n
位。
因为 x
在左移32-n
位后,将高32-n
位的信息全部都清除了,若再右移32-n
位后仍然与原来相同,则说明x
截断成后n
位是不会改变x
原本的数值的,即x
的补码可以表示为n
位。这是因为对于可以用n
位补码表示的数x
,上述过程的算数右移相当于对x
进行扩展,不会改变它的数值信息。
而对于32-n
位如何表示,根据补码的定义(即补码正负数之间的表示的转换关系)可以得出:-n = ~n+1
;从而可以通过上述操作实现函数功能。
函数实现:
int fitsBits(int x, int n) {
int y;
n = ~n + 1;
y = (x<<(32+n))>>(32+n);
return !(x^y);
}
dividePower2函数
函数要求:
函数名 | dividePower2 |
---|---|
参数 | int , int |
功能实现 | 计算x除以2的n次幂 |
要求 | ! ~ & ^ | + << >> |
实现分析:
x
除以2的n次幂,对于有符号整数,需要正负数分开讨论。
当x
为正数时,x
除以2的n次幂,也就是 x>>n
;
当x
为正数时,为使其向0舍入(即向上取整),需要对其进行修正,也即(x+2**n-1)>> n
, 即(x+(1<<n)-1)>>n
;
但是不能使用运算符 -
,但我们可以通过 ~0 = 0xffffffff = -1
实现,也即(x+(1<<n+(~0)))>>n
。
而如何进行正负数的判断呢,我们可以通过 x>>31
得到符号位s:正则s
为0x00000000
,负则s
为0xffffffff
。
通过上述分析,可以得到最终实现代码如下。
函数实现:
int dividePower2(int x, int n) {
return (x+((x>>31)&((1<<n)+(~0))))>>n;
}
negate函数
函数要求:
函数名 | negate |
---|---|
参数 | int |
功能实现 | 返回x的负数 |
要求 | ! ~ & ^ | + << >> |
实现分析:
根据补码中正负数表示的关系(即负数的表示等价于绝对值相同的正数的表示按位取反后加1),可以得知 -x = ~x+1
。
函数实现:
int negate(int x) {
return ~x+1;
}
howManyBits函数
函数要求:
函数名 | howManyBits |
---|---|
参数 | int |
功能实现 | 计算表达x所需的最少位数 |
要求 | ! ~ & ^ | + << >> |
实现分析:
已知补码的最高表示位前面的所有数字应该都是全0(正数)或是全1(负数)。
将原数 x
与其 符号位 异或,正数仍为原数(即x
),负数则为原数按位取反(即~x
),在这里将结果表示为x'
。不考虑符号位的情况下,正数中第一个1(从高位起)为表示所需的最少位数的最高位,负数中第一个0(从高位起)为表示所需的最少位数的最高位,当其取反后,该位成为第一个1。因此,我们可以通过寻找x'
从高位起第一个为1的位次,便可得知,表达x
所需的最少位数。
之后就是用二分法去测试最高位在哪里,比如bit_16=!!(temp>>16)<<4
;结果不为0,说明最高非零位至少在16位以上,我们进一步将高16位留下观察。反之,如果为0,说明最高位介于0和16之间,于是不做处理,等的后面8位处理进行判断。
由此,通过二分法得到若干值,有的是16,8这类数,有的就是0,最后+1的偏置表示符号位,得到需要的位数。
函数实现:
int howManyBits(int x) {
int b16,b8,b4,b2,b1,b0;
/*获得x的符号位*/
int sign = x>>31;
x = x^sign;
/*二分法寻找第一个为1的位次*/
b16 = !!(x>>16)<<4;
x = x>>b16;
b8 = !!(x>>8)<<3;
x = x>>b8;
b4 = !!(x>>4)<<2;
x = x>>b4;
b2 = !!(x>>2)<<1;
x = x>>b2;
b1 = !!(x>>1);
x = x>>b1;
b0 = x;
return b16+b8+b4+b2+b1+b0+1;
}
isLessEqual函数
函数要求:
函数名 | isLessEqual |
---|---|
参数 | int , int |
功能实现 | 计算 x <= y ? |
要求 | ! ~ & ^ | + << >> |
实现分析:
x <= y
由两种情况:
x
与y
同号,y-x
大于等于零x
,y
异号,且y
为非负数
在以上思路的基础上进行分析实现,我们可以首先通过异或操作判断 x
与 y
的符号异同(异或后符号位为1则同号,为0则异号)。然后在同号的情况下计算 sub=y-x
(减法操作在上述实验中已经涉及) 并判断sub
的符号(sub
符号位为0则x<=y
,反则反之);在异号的情况下,直接判断y
的符号,若为0(为正)则x<=y
。
函数实现:
int isLessOrEqual(int x, int y) {
// 计算 y-x
int sub = y + ((~x)+1);
// 用于判断x,y符号情况
int s = (x^y)>>31;
// 情况一
int condition1 = (!s)&(!(sub>>31));
// 情况二
int condition2 = s&(!(y>>31));
return condition1|condition2;
}
intLog2函数
函数要求:
函数名 | intLog2 |
---|---|
参数 | int |
功能实现 | 计算x以2为底的对数,向下取整 |
要求 | ! ~ & ^ | + << >> |
实现分析:
由于真数必须大于零,因此这里不需要考虑负数情况,那么计算x
以2为底的对数,向下取整,就相当于找到x
的补码表示中从最高位起第一个为1的位次。这个操作实现与howManyBits
函数中一致,同样采用二分法即可,此处不再赘述。
函数实现:
int intLog2(int x) {
int log = 0;
log =!!(x>>16)<<4;
log += !!((x>>log)>>8)<<3;
log += !!((x>>log)>>4)<<2;
log += !!((x>>log)>>2)<<1;
log += !!((x>>log)>>1);
return log;
}
表三
floatAbsVal函数
函数要求:
函数名 | floatAbsVal |
---|---|
参数 | unsigned int |
功能实现 | 计算f的绝对值的位级表示 |
要求 | Any integer/unsigned operations incl. ||, &&. also if, while |
实现分析:
实验要求若uf
为NaN
,则原数返回,否则返回器绝对值。
因此我们需要首先判断uf
是否为NaN
,也即检测uf
是否阶码为全1,尾数不为0即可,可通过((uf&0x7f800000)>>23 == 255 && (uf<<9))
语句实现。
如uf
不为NaN
,在其绝对值的位级表示与原本一致,只需要使符号位s=0
即可,可通过(uf&0x7fffffff)
语句实现。
函数实现:
unsigned floatAbsVal(unsigned uf) {
// NaN返回NaN
if((uf&0x7f800000)>>23 == 255 && (uf<<9))
return uf;
else
return uf&0x7fffffff;
}
floatScale1d2函数
函数要求:
函数名 | floatScale1d2 |
---|---|
参数 | unsigned int |
功能实现 | 计算0.5*f的位级表示 |
要求 | Any integer/unsigned operations incl. ||, &&. also if, while |
实现分析:
实现计算0.5*f
的位级表示需要分3种情况考虑:
uf
为NaN
或Infinity
0.5*uf
为规格化值(Exp>1
)0.5*uf
为非规格化值(Exp<=1
)
– uf
为 NaN
或 Infinity
NaN
和 Infinity
乘0.5
后依旧为 NaN
和 Infinity
,直接返回
– 0.5* uf
为规格化值(Exp>1
)
0.5* uf
依旧为规格化值,则可直接通过Exp-1
得到结果
– 0.5*uf
为非规格化值(Exp<=1
)
当Exp<=1
时,0.5*uf
为非规格化值(Exp=0
,uf
为非规格化值,0.5*uf
也为非规格化值;Exp=1
,uf
为规格化值,但0.5 *uf
为非规格化值),则通过将uf
(摘除符号位)右移一位实现,但对于被移出的尾数(即最后一位),需要考虑其舍入,根据向偶舍入原则,仅当末尾两位为11时才需要向前进位,因此可通过语句if((uf&0x3)==0x3)
判断是否需要进位,需要则uf = uf + 0x1
;之后再在最前面加上符号位即可。
0
也包含在这一情况中,可以被正确实现。
函数实现:
unsigned floatScale1d2(unsigned uf) {
// get s,E
int s_ = uf&0x80000000;
int Exp = ((uf&0x7f800000)>>23);
// NaN,Infinity
if(Exp == 255)
return uf;
// 当指数E大于1时,指数减一
if(Exp>1) return (uf&0x807fffff)|((Exp-1)<<23);
// 指数E<=1时,尾数右移一位
else{
if((uf&0x3)==0x3) uf = uf + 0x1;
return ((uf&0x00ffffff)>>1)|s_;
}
}
floatFloat2Int函数
函数要求:
函数名 | floatFloat2Int |
---|---|
参数 | unsigned int |
功能实现 | 计算(int)f的位级表示 |
要求 | Any integer/unsigned operations incl. ||, &&. also if, while |
实现分析:
计算(int)f
的位级表示,根据实验要求,可以分为以下几种情况:
- 超出整型范围(包括
Inf
和NaN
),返回0x80000000
uf
为绝对值小于1的小数(即(-1,1)
区间),返回0
- 符合整型范围的数
要区分上述三种情况和实现计算,我们需要获得uf
的符号位并计算 指数E
和 M
。通过指数 E
可以直接区分出三种情况:当E>30
时,将超出整型范围(因为int
型还需要一个位次来表示符号位,因此E>=31
时会超出范围);当E<0
时,uf
为绝对值小于1的小数;当0<=E<=30
时,符合整型范围。
对于符合整型范围的数,由于经M = (uf&0x007fffff)+0x00800000
操作得到的 M
相当于实际M乘上了2的23次幂,因此,当 E>23
时,M
应当再乘以2的(E-23)
次幂(即M <<= (E_-23)
);否则 M >>=(23-E_)
。最后再根据符号位s
对于 M
做适当的处理即可(正数不变,负数取负)。
函数实现:
int floatFloat2Int(unsigned uf) {
// get s,E and M
int s_ = uf>>31;
int E_ = ((uf&0x7f800000)>>23)-127;
int M_ = (uf&0x007fffff)+0x00800000;
// overflow
if(E_>30) return 0x80000000;
// 0 or abs less than 1
if(E_< 0) return 0;
if(E_ > 23) M_ <<= (E_-23);
else M_ >>= (23-E_);
// positive and negative
if(!s_) return M_;
else return ~M_+1;
}
四、实验总结
在本段总结实验中遇到的问题或困难,以及对应的解决方法;对实验的建议等。
问题:
- 在实现
isLessEqual(int x, int y)
函数时,我最初想采用通过!((y+(~x+1))>>31)
语句来判断y>=x
,但是实践中却无法通过测试,最终是在查找参考资料后,采用上述报告中的分情况讨论的方式实现的。 - 在写这个语句的判断条件的时候
if((uf&0x3)==0x3)
uf = uf + 0x1
我最初的写法是if(uf&0x3 == 0x3)
,当时是考虑到&
的优先级高于==
,是可以实现我想要的逻辑运算的(即(uf&0x3)==0x3
),但是最后测试却不通过,当我加上括号后就通过了,可能是实验的编译环境的底层运算分析不足的原因,但是也更加地提醒了我要规范的进行编程、规范地使用语言,使程序更健壮、可读性更高。