Lab 1 Data Lab
写在前言:这个实验的来源是CSAPP官网:CSAPP Labs ,如果感兴趣的话,可以点击这个链接🔗去下载,Data Lab这个实验可以检验我们对数据的编码以及IEEE754浮点数格式的掌握程度,只有我们充分掌握了CSAPP书上第二章的内容,对于这个实验,可以说是游刃有余了,Data Lab包含整数和浮点数编码两部分,相应的会做出一些限制,主要任务就是让我们用位级操作实现一些功能。
编码限制
INTEGER CODING RULES:
Replace the "return" statement in each function with one
or more lines of C code that implements the function. Your code
must conform to the following style:
int Funct(arg1, arg2, ...) {
/* brief description of how your implementation works */
int var1 = Expr1;
...
int varM = ExprM;
varJ = ExprJ;
...
varN = ExprN;
return ExprR;
}
Each "Expr" is an expression using ONLY the following:
1. Integer constants 0 through 255 (0xFF), inclusive. You are
not allowed to use big constants such as 0xffffffff.
2. Function arguments and local variables (no global variables).
3. Unary integer operations ! ~
4. Binary integer operations & ^ | + << >>
Some of the problems restrict the set of allowed operators even further.
Each "Expr" may consist of multiple operators. You are not restricted to
one operator per line.
You are expressly forbidden to:
1. Use any control constructs such as if, do, while, for, switch, etc.
2. Define or use any macros.
3. Define any additional functions in this file.
4. Call any functions.
5. Use any other operations, such as &&, ||, -, or ?:
6. Use any form of casting.
7. Use any data type other than int. This implies that you
cannot use arrays, structs, or unions.
You may assume that your machine:
1. Uses 2s complement, 32-bit representations of integers.
2. Performs right shifts arithmetically.
3. Has unpredictable behavior when shifting if the shift amount
is less than 0 or greater than 31.
EXAMPLES OF ACCEPTABLE CODING STYLE:
/*
* pow2plus1 - returns 2^x + 1, where 0 <= x <= 31
*/
int pow2plus1(int x) {
/* exploit ability of shifts to compute powers of 2 */
return (1 << x) + 1;
}
/*
* pow2plus4 - returns 2^x + 4, where 0 <= x <= 31
*/
int pow2plus4(int x) {
/* exploit ability of shifts to compute powers of 2 */
int result = (1 << x);
result += 4;
return result;
}
FLOATING POINT CODING RULES
For the problems that require you to implement floating-point operations,
the coding rules are less strict. You are allowed to use looping and
conditional control. You are allowed to use both ints and unsigneds.
You can use arbitrary integer and unsigned constants. You can use any arithmetic,
logical, or comparison operations on int or unsigned data.
You are expressly forbidden to:
1. Define or use any macros.
2. Define any additional functions in this file.
3. Call any functions.
4. Use any form of casting.
5. Use any data type other than int or unsigned. This means that you
cannot use arrays, structs, or unions.
6. Use any floating point data types, operations, or constants.
bitXor
函数的声明和限制如下:
//1
/*
* bitXor - x^y using only ~ and &
* Example: bitXor(4, 5) = 1
* Legal ops: ~ &
* Max ops: 14
* Rating: 1
*/
int bitXor(int x, int y) {
return 0;
}
这道题目限制使用两个操作符:按位取反~、按位与&,根据离散数学的基本等值式:异或可以表示为:
因此答案是:
int bitXor(int x, int y) {
int result = ~(~(~x & y) & ~(x & ~y));
return result;
}
tmin
函数声明如下:
/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
return 0;
}
要求我们得到32位补码整数的最小值,就是0x80000000
,可以使用一个简单的移位实现
答案为:
int tmin(void) {
int result = 1 << 31;
return result;
}
isTmax
函数声明如下:
//2
/*
* isTmax - returns 1 if x is the maximum, two's complement number,
* and 0 otherwise
* Legal ops: ! ~ & ^ | +
* Max ops: 10
* Rating: 1
*/
int isTmax(int x) {
return 0;
}
要求我们判断 x 是否为32位补码整数最大值0x7fffffff
,一种思路是先生成Tmax32
,然后在使用异或 ^ 和逻辑非 ! 操作符判断 x 是否是 Tmax32
,
int isTmax(int x) {
int tmax = (1 << 31) - 1;
return !(x ^ tmax);
}
如果 x=Tmax
,那么表达式 !(x ^ tmax)=1
,因为 a^a=0
,两个相同的数进行异或一定为0。
但是题目要求这里不能使用减法和移位操作,所以要另辟蹊径。
对于 Tmax
,有一个性质:(Tmax+1) + (Tmax+1)=(0x7fffffff+1)+(0x7fffffff+1)=0x80000000+0x80000000=0
,意味着我们可以考虑计算表达式 (x+1)+(x+1)
判断它是否为0就可以了,但是这个性质对于x=-1
也是满足的,所以还需要排除这个情况:
int isTmax(int x) {
int expr = (x + 1) + (x + 1);
// x is Tmax if expr = 0 and x != -1
int istmax = !expr & !!(x + 1);
return istmax;
}
allOddBits
函数声明如下:
/*
* allOddBits - return 1 if all odd-numbered bits in word set to 1
* where bits are numbered from 0 (least significant) to 31 (most significant)
* Examples allOddBits(0xFFFFFFFD) = 0, allOddBits(0xAAAAAAAA) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 12
* Rating: 2
*/
int allOddBits(int x) {
return 0;
}
要求我们判断x的奇数位是否全部设置为了1,因为只需要考虑奇数位,可以先生成一个奇数位掩码0xAAAAAAAA
,位级整数编码要求我们只能使用0-0xff
这种小整数,所以不能直接使用这种大整数,可以通过0xAA
移位合并起来,然后生成掩码后,按位与 x,将结果和这个掩码判断,如果相等,那么所有奇数位为1:
int allOddBits(int x) {
int mask = (0xAA << 24) | (0xAA << 16) | (0xAA << 8) | 0xAA;
int result = !((mask & x) ^ mask);
return result;
}
negate
函数声明如下:
/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
return 0;
}
要求计算表达式 -x,但不使用减号,很简单,根据位级整数的性质:-x=~x+1
,
int negate(int x) {
int result = ~x + 1;
return result;
}
isAsciiDigit
函数声明如下:
//3
/*
* isAsciiDigit - return 1 if 0x30 <= x <= 0x39 (ASCII codes for characters '0' to '9')
* Example: isAsciiDigit(0x35) = 1.
* isAsciiDigit(0x3a) = 0.
* isAsciiDigit(0x05) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 3
*/
int isAsciiDigit(int x) {
return 0;
}
这个函数要求我们判断输入x是否为Ascii的数字,就是判断数字x是否在这个范围内:0x30<=x<=0x39
,分析如下:
0x30=0011 0000
并且0x39=0011 1001
,Ascii数字的编码,形式为0x3.
,第5,6比特均为1,高位比特均为0,因此可以利用这一特性,排除一些选项,可生成掩码mask=0xffffff00
。- 在第一个条件后,范围变成了
0x30 --- 0x3f
,显然有一些是不满足的,因为Ascii数字编码最大值为0x39
所以我们可以计算表达式x+6
,如果大于了0x3f
,那么x不是Ascii数字,这时可以使用1的掩码mask
判断表达式x+6
的结果第5,6比特是否都为1
int isAsciiDigit(int x) {
// Mask = 0xffffff00
int mask = ~0 + (~0xffff + 1);
int flag1 = !((x & mask) ^ 0x30);
int flag1 = !(((x + 6) & mask) ^ 0x30);
return flag1 & flag2;
}
conditional
函数声明如下:
/*
* conditional - same as x ? y : z
* Example: conditional(2,4,5) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 16
* Rating: 3
*/
int conditional(int x, int y, int z) {
return 0;
}
这个函数要求我们使用位级操作实现三目运算符,这个类似于一个多路复用器 Multiplex,根据 x 的值选择y和z,如果x不为0,结果为y,否则为z。可利用x生成一个掩码:mask=(!x << 31) >>31
,然后利用按位或|选择结果。
int conditional(int x, int y, int z) {
// mask = 0xfffffff if x != 0 else mask = 0
int mask = (!x << 31) >> 31;
int result = (~mask & y) | (mask & z);
}
WE就是多路复用器的写使能,用来判断选择哪一个输入,和这里的三目运算符原理是一样的。
isLessOrEqual
函数声明如下:
/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
return 0;
}
这个函数要求实现一个运算符 <=
,我们可以考虑计算表达式 x-y
,判断它是否大于0,从而得出x和y的大小关系,前提是x-y
不能溢出,可以分以下3个情况:
- x>0并且y<0,返回1
- x<0并且y>0,返回0
- x>0, y>0或者x<0, y<0
- 计算表达式
x-y
- 判断与0的关系
- 计算表达式
int isLessOrEqual(int x, int y) {
// Get xsig and ysig
int xsig = (x >> 31) & 1;
int ysig = (y >> 31) & 1;
// Compute x - y
int z = x + ~y + 1;
int lflag = (z >> 31) & 1;
// 相等标记
int zflag = !(x ^ y);
// result=1 if x>0 且 y<0 或者 x和y同号且小于标记或零标记为1
int result = (xsig & (~ysig & 1)) | (!(xsig ^ ysig) & (lflag | zflag));
return result;
}
logicalNegate
函数声明如下:
/*
* logicalNeg - implement the ! operator, using all of
* the legal operators except !
* Examples: logicalNeg(3) = 0, logicalNeg(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int logicalNeg(int x) {
return 0;
}
要求实现逻辑非运算符,可以利用以下性质:
x==~x+1
,只有当 x为0
或者Tmin32=0x80000000
- 计算相反数表达式
~x+1
,排除x=0x80000000
这种情况。- 可以通过它们的符号位进行判断
int logicalNeg(int x) {
int y = (~x + 1);
// 只有当 x 和 y=-x 的符号位均为0 结果才为1
int result = ((~x & ~y) >> 31) & 1;
return result;
}
howManyBits
函数声明如下:
/* howManyBits - return the minimum number of bits required to represent x in
* two's complement
* Examples: howManyBits(12) = 5
* howManyBits(298) = 10
* howManyBits(-5) = 4
* howManyBits(0) = 1
* howManyBits(-1) = 1
* howManyBits(0x80000000) = 32
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int howManyBits(int x) {
return 0;
}
这个函数要求我们获取能够以二进制补码表示 x 的最小位长,这里涉及到一个截断的问题,
假设x表示模式为:
x
=
[
x
31
,
x
30
,
.
.
.
,
x
1
,
x
0
]
x=[x_{31},x_{30},...,x_1,x_0]
x=[x31,x30,...,x1,x0]
假设x截断为n位二进制数:
x
′
=
[
x
n
−
1
,
x
n
−
2
,
.
.
.
,
x
1
,
x
0
]
x^{'}=[x_{n-1}, x_{n-2},...,x_1,x_0]
x′=[xn−1,xn−2,...,x1,x0]
如果两个数相等,那么一定有:
x
32
=
x
31
=
⋯
=
x
n
=
x
n
−
1
x_{32}=x_{31}=\cdots=x_n=x_{n-1}
x32=x31=⋯=xn=xn−1
所以我们可以有这种思路:
- 如果
(x << n) >> n == x
,那么它可以被32-n
位的二进制补码数精确表示。 - 由于我们不能使用循环迭代的方式去迭代n,找到临界的n就行了,要另辟蹊径。
- 考虑使用二分查找
- 第一次检查n=16
- 第二次检查n=8
- 依次类推
- 直到n=1为止
int howManyBits(int x) {
// Use binary search
int tag16 = 0;
int tag8 = 0;
int tag4 = 0;
int tag2 = 0;
int tag1 = 0;
int y = 0;
// if tag16 = 1, it means that it can be represented by less 16 bit.
y = (x << 16) >> 16;
tag16 = !(x ^ y);
x = x << (tag16 << 4);
y = (x << 8) >> 8;
tag8 = !(x ^ y);
x = x << (tag8 << 3);
y = (x << 4) >> 4;
tag4 = !(x ^ y);
x = x << (tag4 << 2);
y = (x << 2) >> 2;
tag2 = !(x ^ y);
x = x << (tag2 << 1);
y = (x << 1) >> 1;
tag1 = !(x ^ y);
// result = 32 - (tag16 * 16 + tag8 * 8 + tag4 * 4 + tag2 * 2 + tag1)
return 32 + ~((tag16 << 4) + (tag8 << 3) + (tag4 << 2) + (tag2 << 1) + tag1) + 1;
}
实际上上面就是将一个二分查找的一个循环结果使用循环展开的方式,至于为什么使用二分查找,是因为题目限制了操作符个数,否则可以穷举n=1至n=32,不过这样太复杂且操作符太多。
floatScale2
函数声明如下:
//float
/*
* floatScale2 - 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
*/
unsigned floatScale2(unsigned uf) {
return 2;
}
浮点编码要求我们使用unsigned为类型的表示的浮点数进行一些操作,我们必须掌握IEEE754浮点数格式才能够在位级上使用unsigned进行浮点操作,要求比较高。
这个函数要求我们实现的就是 2 * uf
这个表达式,我们必须要考虑以下情况:
- Case 1:当 uf 是NaN或者Inf,那么返回 uf
- Case 2:当 uf 的阶码
exp=254
, 那么2 * uf = INF
- Case 3:如果 uf 的阶码
0 < exp < 254
,那么只需要将 uf 的阶码+1更新其阶码域即可 - Case 4:如果 uf 为阶码
exp=0
,也就是 uf 为非规格化数- 不能简单地让阶码+1,因为非规格化数表示为 V = ( − 1 ) s × 0. f × 2 1 − b i a s , b i a s = 127 V=(-1)^s \times 0.f\times2^{1-bias},bias=127 V=(−1)s×0.f×21−bias,bias=127。
- 只需要将 frac左移一位即可,但是要考虑如下情况:
- 因为frac是23位的,如果frac的第23比特为1,那么就不能左移,因为会超出范围,这时应该将阶码+1,变成一个规格化数
- 否则,直接frac左移一位更新frac域即可
unsigned floatScale2(unsigned uf) {
// Extract s
unsigned s = uf & 0x80000000;
// Extract exp
int exp = (uf >> 23) & 0xff;
// Extract frac
int frac = 0x7fffff & uf;
unsigned result = 0;
if (exp == 255) {
// NaN or infinity
result = uf;
} else if (exp == 254) {
// 2 * f == inf
exp = 255;
frac = 0;
result = s | (exp << 23) | frac;
} else if (0 < exp && exp < 254) {
// 2 * f can be represented as normalized number
exp += 1;
result = s | (exp << 23) | frac;
} else {
// exp == 0, 2 * f can be represented as denormalized number or a normalized number.
if (frac < 0x800000) {
frac <<= 1;
result = s | (exp << 23) | frac;
} else {
// 2 * f can't be represented as a denormalized number.
exp = 1;
result = s | (exp << 23) | frac;
}
}
return result;
}
floatFloat2Int
函数声明如下:
/*
* floatFloat2Int - Return bit-level equivalent of expression (int) f
* for floating point argument f.
* Argument is passed as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point value.
* Anything out of range (including NaN and infinity) should return
* 0x80000000u.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
int floatFloat2Int(unsigned uf) {
return 2;
}
这个函数要求我们实现一个从浮点数到整数的强制转换操作符 (int)
,但是浮点数强制转换为整数,有可能转换不精确,因为浮点数的精度有限以及整数int表示范围小于浮点数。
需要考虑下列不同情况:
- uf 为 NaN或者Inf,即阶码位
exp=255
,返回0x80000000u
- uf 为 非规格化数,阶码位
exp=0
,返回 0- 因为非规格化数的表示值为: V = ( − 1 ) s × 0. f × 2 1 − b i a s , b i a s = 127 V=(-1)^s \times 0.f\times2^{1-bias},bias=127 V=(−1)s×0.f×21−bias,bias=127,不管怎么舍入,都只能舍入到0
- uf 为规格化数,
V
=
(
−
1
)
s
×
1.
f
×
2
e
x
p
−
b
i
a
s
,
b
i
a
s
=
127
V=(-1)^s\times 1.f\times2^{exp-bias},bias=127
V=(−1)s×1.f×2exp−bias,bias=127考虑以下情况
- exp<bias,舍入到0
- exp=bias,判断尾数frac的第23位是否为1,如果是,舍入到2,否则舍入到1。
- exp>bias,需要判断是否超出范围
- 此时转换为整数的结果为
result = 1frac << (exp - bias)
- 因为1frac已经占据了24比特,如果移位量
exp-bias
大于等于8,那么就超出整数的表示范围了,Overflow - 否则,直接移位即可。
- 此时转换为整数的结果为
int floatFloat2Int(unsigned uf) {
int bias = 127;
int exp = (uf >> 23) & 0xff;
int frac = uf & 0x7fffff;
int s = uf & 0x80000000;
int result = 0;
int m = 0;
if (exp == 255) {
// NaN or Infinity.
result = 0x80000000u;
} else if (exp == 0) {
// Unormalized number(too small)
result = 0;
} else if (exp < bias) {
// Normalized number but small to be rounded to zero
result = 0;
} else if (exp == bias) {
// V = (-1)^s * 1.f
if (frac >= 0x400000) {
// round to even
result = 2;
} else {
result = 1;
}
result = s ? -result : result;
} else if (exp > bias) {
if (exp - bias >= 8) {
// Overflow
result = 0x80000000u;
} else {
// 0 < exp - bias < 8
m = (1 << 23) | frac;
result = m << (exp - bias);
result = s ? -result : result;
}
}
return result;
}
floatPower2
函数声明如下:
/*
* floatPower2 - Return bit-level equivalent of the expression 2.0^x
* (2.0 raised to the power x) for any 32-bit integer x.
*
* The unsigned value that is returned should have the identical bit
* representation as the single-precision floating-point number 2.0^x.
* If the result is too small to be represented as a denorm, return
* 0. If too large, return +INF.
*
* Legal ops: Any integer/unsigned operations incl. ||, &&. Also if, while
* Max ops: 30
* Rating: 4
*/
unsigned floatPower2(int x) {
return 2;
}
这个函数要求计算表达式 2 x 2^x 2x,看这个要求貌似很简单,但实际上没那么简单,要考虑比较多的因素,因为x作为浮点数的阶码域,它的范围是有限的,考虑以下情况:
- 如果 x >= 128,结果太大,返回 +INF
- 如果 -126 <= x <= 127,那么表示为规格化数
- 计算
exp= x + 127
,结果为:(exp << 23)
- 计算
- 如果 -149 <= x <= -127,表示为非规格化数,
- 因为非规格化数的特殊形式,非规格化数最小时,frac=1
- 最大时,frac的第23位为1,其余为0
- 因此可以在规格化数的范围扩展23个数量级到达 2 − 149 2^{-149} 2−149。
- 此时,计算
k=x+149
- 得到尾数
frac = 1<<k
- 此时frac就是结果
- 如果 x<=-150,那么x太小了,返回0即可
unsigned floatPower2(int x) {
unsigned bias = 127;
unsigned exp = 0;
unsigned result = 0;
int k = 0;
if (x >= 128) {
// x is too big.
return 0x7f800000u;
} else if (-126 <= x && x <= 127) {
// 2.0^x can be represented as normalized number.
exp = x + bias;
result = exp << 23;
return result;
} else if (-149 <= x && x <= -127) {
// 2.0^x can be represented as denormalzied number.
exp = 0;
// Compute shift amount 0 <= k <= 22
k = x + 149;
result = 1 << k;
return result;
} else {
// x is too small. x<=-150
return 0u;
}
}
总结
以上所有题目的方法均为自己所想,可能不是最简的,但是经过测试,这些解决方法都能通过测试。这个实验很有趣,建议自行完成,自己去思考有助于真正掌握数据的位级编码,比如二进制补码整数和无符号整数在运算过程中是具有相同的位级形式,区别仅仅是表示的不同;再如浮点数的IEEE754编码格式,分为4种情况:NaN、无穷大、规格化数、非规格化数等等,它们的编码格式不同,表示值的格式也不同。