目录
下载实验用的文件戳这里
bitXor
(以下推导方法改编自 StackOverflow 用户 @Mike 的回答。比心~)
这道题呢,我们可以列一张表:
x x x | y y y | x x x ^ y y y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
我们要怎么做才能找到这三者之间的关系呢?既然题中要求了要用 & 或者 ~ 来实现,我们就给 “&” 也列一张表:
x x x | y y y | x x x & y y y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
我相信,你一定在离散数学或者数字电路这门课里接触过一种运算叫”或非“。那么我们给 “或非”(NOR) 也列一张表:
x x x | y y y | x x x NOR y y y |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 0 |
之后我们把这三张表放在一起:
x x x & y y y | x x x NOR y y y | x x x ^ y y y |
---|---|---|
0 | 1 | 0 |
0 | 0 | 1 |
0 | 0 | 1 |
1 | 0 | 0 |
观察到了吗?前两列做或非运算是不是就能得到第三列?所以我们可以把异或运算写成这种形式:
x
X
O
R
y
=
(
x
A
N
D
y
)
N
O
R
(
x
N
O
R
y
)
x\; \mathrm{XOR} \;y = (x \;\mathrm{AND} \;y)\; \mathrm{NOR} \;(x \;\mathrm{NOR} \;y)
xXORy=(xANDy)NOR(xNORy)
想一想,或非运算可以怎样用 & 和 ~ 两个运算符表示呢?
这个不难。很显然,
x
N
O
R
y
=
\qquad\qquad\qquad x\; \mathrm{NOR} \; y =
xNORy= ~
x
x
x & ~
y
y
y
把这个表示代入上式,就能得到下面的C语言表达式:
int bitXor(int x, int y) {
return ~(x & y) & ~(~x & ~y);
}
成功啦。
tmin
很常规的题啦~ 考的是定义。
int tmin(void) {
return 1 << 31;
}
isTmax
这道题要我们比较一个数是不是和int可以表示的最大值相等。当然最自然的办法是先凑出 0x7fffffff,之后和
x
x
x 比较。可是很遗憾,这道题不允许我们使用左移右移操作,因此 0x7fffffff 是不容易凑出的。
那应该怎么办呢?
我们知道,0x7fffffff 取反之后的结果 0x80000000 有一个特点:取它的补码,结果仍为 0x80000000. 所以我们借助这个特点来写这个函数。注意, 0 取补码的结果也依然为 0. 把为 0 的情况排除掉就可以了。
int isTmax(int x) {
int negate1 = ~x; // 取反
int negate2 = ~negate1 + 1; // 取取反结果的补码
return (!(negate2 ^ negate1)) & !!negate1;
// 取过补码之后,值有变化吗? negate1 为零吗?
}
allOddBits
按照题中的要求,只要参数 x x x 的所有奇数位都为一,就可以返回 1,而对它的偶数位没有具体要求。既然没有要求,那我们的发挥空间就很大了——直接把 x x x 的偶数位全变成一,之后把 x x x 按位取反,再和零比较是否相等不就可以了吗?所以这个函数可以这样写:
int allOddBits(int x) {
int combination = x |
(0x55 | (0x55 << 8) | (0x55 << 16) | (0x55 << 24));
// 拼出0x55555555,再和x按位或
int ans = !(~combination ^ 0);
return ans;
}
negate
基础的基础的基础!!!
int negate(int x) {
return ~x + 1;
}
isAsciiDigit
这道题的本质是让我们判断 x x x 是不是在0x30和0x39之间。用位运算怎么比较大小呢?两数作差,取符号位就可以了。
int isAsciiDigit(int x) {
int isAbove0x30 = !(((x + ((~0x30) + 1)) >> 31) & 1);
int isBelow0x39 = !(((0x39 + (~x + 1)) >> 31) & 1);
return isAbove0x30 & isBelow0x39;
}
logicalNeg
如果一个数
x
x
x非零,那么它的相反数
−
x
-x
−x也一定不为零,对吧?
那么,
x
x
x和
−
x
-x
−x, 一定有一个的符号位为一,对吧?所以,只要我们取
x
x
x的相反数,把它和原来的
x
x
x拼在一起,再取符号位,是不是就可以把零和其他数区分出来了?区分出来之后呢,我们只要把刚才那个拼在一起的结果右移31位,是不是就得到-1(或者是零)了?再加上一,不就达到要求了嘛。
int logicalNeg(int x) {
int combination = x | (~x + 1);
return (combination >> 31) + 1;
}
conditional
这道题让我们模拟三目运算符——如果参数 x x x 为不零,就返回参数 y y y ; 如果为零,就返回参数 z z z . 那么如何控制返回哪个值呢?我们可以先做一个类似掩码的东西——这个数的每一位,不是全为零,就是全为一,根据 x x x 的取值而定。做这个掩码的一种方法是借助左移和右移,就像下面这样:
int mask = ((!!x) << 31) >> 31;
另外一种方法是凑-1:
int mask = ~(!!x) + 1;
结果是一样的——如果
x
x
x 不是零,mask就是 0xffffffff,反之就是 0。
这个mask怎么用?借助取反和按位或就可以:
int ans = (~mask & z) | (mask & y);
所以最终的代码是这样的:
int conditional(int x, int y, int z) {
int mask = ((!!x) << 31) >> 31;
int ans = (~mask & z) | (mask & y);
return ans;
}
isLessThanOrEqual
起初你可能会觉得,这道题像上面那样作差就ok:
int isLessOrEqual(int x, int y) {
int difference = y + (~x + 1);
int sign = (difference >> 31) & 1;
return !sign;
}
但是!但是!但是!你会发现这个当
y
y
y 和
x
x
x 只要有一个是int可以表示的最大值或者最小值的时候,函数就无法返回正确的值!
为什么呢?首先,如果
x
x
x 是0x80000000,那么这个数取相反数用int是表示不出来的!这样不管
y
y
y 是多少,difference的符号位总是1!其次,如果
x
x
x、
y
y
y 都是很接近int极限的数,并且一正一负,那么算difference时几乎一定会溢出!这时符号位是多少是没有意义的!
因此,正确的做法是,除了要判断difference的符号,也要对比
x
x
x、
y
y
y 的符号:如果
y
y
y 为正,
x
x
x 为负,就不需要多余的比较了。
int isLessOrEqual(int x, int y) {
int difference = (unsigned)y + (unsigned)(~x + 1);
int sign_x = (x >> 31) & 1;
int sign_y = (y >> 31) & 1;
// 取x、y的符号
int is_xy_share_same_sign = !(sign_x ^ sign_y);
// x、y符号是否相同呢?
int sign = (difference >> 31) & 1;
return (!sign & is_xy_share_same_sign) |
// x、y符号相同时才根据difference的符号判断大小
(!is_xy_share_same_sign & (((sign_y + (~sign_x + 1)) >> 31) & 1));
// x、y符号不相同就直接看是不是y为正、x为负
}
howManyBits
看到这个函数的要求,不知道你有什么想法?
如果一下子没有思路的话,也没有关系,我们先回忆一下有关补码的基础知识:
我们设n位位向量
ω
⃗
=
(
ω
n
−
1
,
ω
n
−
2
,
…
,
ω
2
,
ω
1
,
ω
0
)
\vec{\omega} = (\omega_{n-1}, \omega_{n-2}, \ldots ,\omega_{2}, \omega_{1}, \omega_{0})
ω=(ωn−1,ωn−2,…,ω2,ω1,ω0). 其中对于任意的
k
∈
{
0
,
1
,
…
,
n
−
1
}
k \in \{0, 1, \ldots, n-1\}
k∈{0,1,…,n−1} ,有
ω
k
∈
{
0
,
1
}
\omega_k \in \{0, 1\}
ωk∈{0,1}. 想想看,按照补码的运算方式,
ω
⃗
\vec{\omega}
ω 这个位向量可以表示出的最大数是什么呢?没错,就是当
ω
n
−
1
=
0
\omega_{n-1} = 0
ωn−1=0, 其余的
ω
k
\omega_k
ωk全部等于一时,下面这个和式:
Ω
max
=
2
n
−
2
+
2
n
−
3
+
…
+
2
1
+
2
0
\Omega_{\max} = 2^{n-2} + 2^{n-3} + \ldots + 2^{1} + 2^{0}
Ωmax=2n−2+2n−3+…+21+20.
就是等比数列求和嘛!所以最终我们有:
Ω
max
=
1
×
(
1
−
2
n
−
1
)
1
−
2
=
2
n
−
1
−
1.
\Omega_{\max} = \frac{1\times(1-2^{n-1})}{1-2} = 2^{n-1}-1.
Ωmax=1−21×(1−2n−1)=2n−1−1.
同样,我们可以知道,这个位向量可以表示出的最小的数是
Ω
min
=
−
2
n
−
1
\Omega_{\min} = -2^{n-1}
Ωmin=−2n−1.
好啦,回忆结束。现在再来想一想,这些基础知识和这个函数要算的东西之间,有什么样的联系呢?
相信你一定想到了——解方程就ok. 如果我们在
x
x
x 为非负数时,令
Ω
max
=
x
\Omega_{\max} = x
Ωmax=x; 在
x
x
x 为负数时,令
Ω
min
=
x
\Omega_{\min} = x
Ωmin=x, 根据上面的等式解出
n
n
n 的值,再向上取整,不就得到 howManyBits 要求返回的值了吗?如果用数学语言表达的话,howManyBits 就是下面这个东西:
h
o
w
M
a
n
y
B
i
t
s
(
x
)
=
{
⌈
log
2
(
x
+
1
)
⌉
+
1
,
x
≥
0
⌈
log
2
(
−
x
)
⌉
+
1
,
x
<
0
howManyBits(x)= \left\{ \begin{array}{ll} \lceil\log_2 (x+1)\rceil+1, x\ge0\\ \lceil\log_2(-x)\rceil + 1, x<0 \end{array} \right.
howManyBits(x)={⌈log2(x+1)⌉+1,x≥0⌈log2(−x)⌉+1,x<0
然而怎么用位运算实现呢?
像用位运算取以二为底的对数之类看着就基础的操作,一定有前人实现好了。所以我们直接去 StackOverflow 上搜索。果不其然,我们搜到了相似的问题,往下翻就可以看到下面这段代码(来自 @phuclv 的回答):
unsigned int v; // 32-bit value to find the log2 of
register unsigned int r; // result of log2(v) will go here
register unsigned int shift;
r = (v > 0xFFFF) << 4; v >>= r;
shift = (v > 0xFF ) << 3; v >>= shift; r |= shift;
shift = (v > 0xF ) << 2; v >>= shift; r |= shift;
shift = (v > 0x3 ) << 1; v >>= shift; r |= shift;
r |= (v >> 1);
(事实上,这段代码的最初来源是这里。根据网站中的说明,csapp 的作者之一 Bryant 验证过其中展示代码的正确性。)
其实我们还可以给它重新排下版,加上注释:
unsigned int log2(unsigned int v)
{
unsigned int r; // result of log2(v) will go here
unsigned int shift;
r = (v > 0xffff) << 4;
// v大于0xffff吗?如果是,就把r的第五位设成1.
// 0x10000(65536)取以二为底的对数,结果是0x10(0b10000).
// 0xffffffff取以二为底的对数,结果是0x1f(去尾)(0b11111).
// 因为v是unsigned int,所以r从第六位开始,之后的任何一位都不可能出现一。
// 大于号不能用?不要紧,把括号里的内容改成(!!(v >> 16))就可以了。
// 下面的改法与这里相似。
v >>= r;
// 大于2^r的部分检查完了。给v除上2^r, 检查余下的部分。
shift = (v > 0xff) << 3;
// v大于0xff吗?如果是,就把r的第四位设成1.
// 为什么是0xff?原因和上面类似——只要v/2^r的值介于0xff和0xffff之间,
// 结果的第四位就一定是一。
v >>= shift;
r |= shift;
shift = (v > 0x7) << 2;
v >>= shift;
r |= shift;
shift = (v > 0x3) << 1;
v >>= shift;
r |= shift;
r |= (v >> 1);
// 到了这一步,v的取值只可能是0、1、2.
// 如果v等于2呢,就把r的第一位设成1.
return r;
// 算完就返回啦~不过返回的结果是去尾的。
}
虽然
log
2
x
\log_2x
log2x 的图像是条曲线,但是
⌊
log
2
x
⌋
\lfloor \log_2x\rfloor
⌊log2x⌋ 可就不是了~ 因此这个函数的基本原理就是找到参数
v
v
v 对应的范围,之后返回对应的值。更详细的注释都在上面的代码里了~
但是这个 lab 的要求是不能用除了 int 之外的数据类型……怎么办呢?如果我们仅仅是简单地将 unsigned int 改成 int,将会导致
v
v
v 的第 31 位为一时,
r
r
r 中的结果不正确——随着前面我们不停的对
v
v
v 算术右移,
v
v
v 的高位会被 1 填满。那么,在最后一步计算 r | (v >> 1) 时,
r
r
r 的高位自然也会被跟着填满。解决方案很简单——在这一步只取
v
v
v 的最低一位就可以了。
那么向上取整怎么做呢?我们知道,上面 log2 函数返回的值,如果是被取过整的话,
2
r
2^r
2r 肯定不会还等于原来的
v
v
v 。这样看,向上取整的操作就很简单了——算
2
r
2^r
2r 的值,如果不等于原始的
v
v
v 值,就说明需要向上取整。至于
x
x
x 大于等于零和小于零时代入的值不同的问题,我们直接套用前面 conditional 函数里用过的逻辑。
所以,最后我们的函数是这样实现的:
int howManyBits(int x) {
int mask = x >> 31;
int value = (mask & (~(x + ~0))) | (~mask & (x + 1));
// 这是 conditional 里用过的逻辑
int processed_x = value;
// 下面几行是取对数的代码
int r; // result of log2(value) will go here
int shift;
r = (!!(value >> 16)) << 4; value >>= r;
shift = (!!(value >> 8)) << 3; value >>= shift; r |= shift;
shift = (!!(value >> 4)) << 2; value >>= shift; r |= shift;
shift = (!!(value >> 2)) << 1; value >>= shift; r |= shift;
r |= ((value >> 1) & 1);
// 最后只取 value 的低位
r += !!(processed_x ^ (1 << r));
// 需要向上取整吗?如果需要,就把r的值加上一。
r += 1;
return r;
}
这样就可以啦。当然网上其他题解中的办法比上面的办法更直接,不过本质都是取以二为底的对数。
floatScale2
加油加油加油!前面整数部分的题都已经做完啦~ 下面的浮点部分,因为可以用 if 语句处理输入,相对就容易多了~
要把规格化的浮点数乘二,只要把阶码加一,再把加一之后的值拼回去。
对于非规格化的浮点值,我们直接把它的小数部分左移一位,再拼回去。
你可能会担心:如果左移小数部分,导致小数部分高位的一溢出到了阶码的低位,还能不能得到正确的结果呢?其实这时的结果也是正确的。假如我们设移动之前的 uf 是下面这个位向量:
u
f
⃗
=
(
s
,
0
,
…
,
0
⏟
8
个
0
,
f
22
,
f
21
,
…
,
f
0
)
\vec{uf}=(s, \underbrace{0, \ldots, 0}_{8个0}, f_{22}, f_{21}, \ldots, f_0)
uf=(s,8个0
0,…,0,f22,f21,…,f0)
这时,
E
=
−
126
E=-126
E=−126
M
=
f
22
⋅
2
−
1
+
f
21
⋅
2
−
2
+
…
+
f
0
⋅
2
−
23
M=f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}
M=f22⋅2−1+f21⋅2−2+…+f0⋅2−23
于是
u
f
⃗
\vec{uf}
uf 表示的值就是
v
=
(
−
1
)
s
×
2
−
126
×
(
f
22
⋅
2
−
1
+
f
21
⋅
2
−
2
+
…
+
f
0
⋅
2
−
23
)
v = (-1)^s \times 2^{-126} \times (f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23})
v=(−1)s×2−126×(f22⋅2−1+f21⋅2−2+…+f0⋅2−23)
那么,左移一位之后是什么情况呢?
这时,
E
E
E 依然是
−
126
-126
−126(对应溢出的情况), 但别忘了,此时 uf 已经是规格化的浮点数了,所以
M
M
M 表示的值是这个:
M
=
1
+
f
21
⋅
2
−
1
+
f
20
⋅
2
−
2
+
…
+
f
0
⋅
2
−
22
M= 1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}
M=1+f21⋅2−1+f20⋅2−2+…+f0⋅2−22
此时
u
f
⃗
\vec{uf}
uf 表示的值是
v
′
=
(
−
1
)
s
×
2
−
126
×
(
1
+
f
21
⋅
2
−
1
+
f
20
⋅
2
−
2
+
…
+
f
0
⋅
2
−
22
)
v' = (-1)^s \times 2^{-126} \times (1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22})
v′=(−1)s×2−126×(1+f21⋅2−1+f20⋅2−2+…+f0⋅2−22)
我们给这两个数作比:
v
′
v
=
1
+
f
21
⋅
2
−
1
+
f
20
⋅
2
−
2
+
…
+
f
0
⋅
2
−
22
f
22
⋅
2
−
1
+
f
21
⋅
2
−
2
+
…
+
f
0
⋅
2
−
23
=
1
+
f
21
⋅
2
−
1
+
f
20
⋅
2
−
2
+
…
+
f
0
⋅
2
−
22
2
−
1
+
f
21
⋅
2
−
2
+
…
+
f
0
⋅
2
−
23
=
2
\Large \begin{array}{ll} \frac{v'}{v}&=\ \frac{1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}}{f_{22} \cdot 2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}}\\\\ &=\ \frac{1 + f_{21} \cdot 2^{-1} + f_{20} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-22}}{2^{-1} + f_{21} \cdot 2^{-2} + \ldots + f_0 \cdot 2^{-23}}\\\\ &=\ \normalsize2 \end{array}
vv′= f22⋅2−1+f21⋅2−2+…+f0⋅2−231+f21⋅2−1+f20⋅2−2+…+f0⋅2−22= 2−1+f21⋅2−2+…+f0⋅2−231+f21⋅2−1+f20⋅2−2+…+f0⋅2−22= 2
神奇嘛~ 这就是说,根据神奇的 IEEE 754 标准,对于把非规格浮点值乘二的操作,我们根本无需担心小数部分溢出的情况——直接左移一位就可以了(不过如果乘的不是二,就不能直接左移了)。
所以,最终我们的代码是这样写的:
unsigned floatScale2(unsigned uf) {
int exponent = (uf >> 23) & 0xff; // 取阶码
int fraction = uf & (0xff | (0xff << 8) | (0x7f << 16));
// 取小数部分
int sign = uf & (1 << 31); // 取符号位
if (exponent != 0xff) // 是无穷大或者 NaN 吗?
{
if (exponent == 0) // 非规格化的情况
{
fraction <<= 1;
return fraction | sign;
// 左移一位之后和符号位拼在一起
}
exponent += 1; // 阶码加一
if (exponent == 0xff)
uf = sign;
// 无穷大?清掉 uf 除了符号位之外的部分
exponent <<= 23;
uf &= ~(0xff << 23);
// 先抹掉 uf 原来的阶码
uf |= exponent;
// 再把新阶码拼上
return uf;
}
// 是无穷大或者 NaN 就直接返回 uf
return uf;
}
floatFloat2Int
我们知道,对于参数 uf,它所对应的值是这样算出来的:
V
=
(
−
1
)
s
×
M
×
2
E
V = (-1)^s \times M \times 2^E
V=(−1)s×M×2E
其中
M
M
M 是一个二进制小数。类比十进制,你觉得
M
×
2
E
M \times 2^E
M×2E 会怎样让
M
M
M 的值变化呢?
我们先从十进制的情况入手。假设
x
x
x 是一个十进制小数(比如,3.1415926535)。之后,我们给
x
x
x 乘上
1
0
n
10^n
10n(也就是
(
9
+
1
)
n
(9 + 1)^n
(9+1)n,对应前面的2)。你会发现小数点向后移动了
n
n
n 位对不对。那么放到二进制里,情况也是一样的。乘以
2
E
2^E
2E, 就相当于把
M
M
M 的小数点向后移动
E
E
E 位。这道题的要求是把浮点数转换成整数,因此我们只要确定小数点会被移动到哪个位置,之后取出小数点之前的所有位,就是
V
V
V 转换成整数之后的值。
int floatFloat2Int(unsigned uf) {
int sign = uf >> 31;
int exponent = (uf >> 23) & 0xff;
int E = exponent - 127;
int fraction = uf & (0xff | (0xff << 8) | (0x7f << 16));
int offset = 23 - E;
// 算偏移量:应当把 fraction 右移多少位才能去掉小数点之后的部分?
// offest 大于零,右移;小于零,左移
if (E < 0)
// 非规格化的情况也包含其中了,因此 M 一定等于 1 + f
return 0;
if (exponent == 0xff || offset < -8)
return (1 << 31);
// 如果是非规格化的浮点值,或者无穷大,
// 返回 0x80000000
if (offset < 0)
fraction <<= (-offset);
if (offset >= 0)
fraction >>= offset;
fraction |= (1 << E); // 相当于 1 + f 中的 1.
if (sign) // 如果原数是负的,就给 fraction 取相反数
fraction = -fraction;
return fraction;
}
floatPower2
这道题要求我们算 2. 0 x 2.0^x 2.0x. 我们按 x x x 的取值范围分成几种情况讨论:
-
0
≤
x
≤
128
\quad0 \leq x \leq 128
0≤x≤128
这种情况对应的是中规中矩的规格化的情况。只要算出 exponent 是多少,再把它左移到合适的位置就可以了。 -
x
>
128
\quad x > 128
x>128
这种情况下,exponent 算出来已经大于 255 了,用一个字节是放不下的。返回无穷大就好。 -
−
126
≤
x
<
0
\quad-126 \leq x < 0
−126≤x<0
x x x 等于 -126 时,我们来到了规格化与非规格化的分界线——此时阶码为一,表示的值为 2 − 126 2^{-126} 2−126. 和上面一样,也是算出 exponent 是多少,再左移到合适的位置就可以了。 -
−
149
≤
x
<
−
126
\quad-149 \leq x < -126
−149≤x<−126
非规格化的值可不可以表示 2 x 2^x 2x 呢?答案是肯定的。不过这里的计算方法和上面有所不同。要表示 2 − 127 2^{-127} 2−127,需要把返回值的第 22 位(小数部分的最高位)设成一,保持其他位为零。要表示 2 − 128 2^{-128} 2−128 或者更小的数,只要在刚才的基础上把这个返回值右移对应的位数就可以了。 -
x
<
−
149
\quad x < -149
x<−149
x x x 小于 − 149 -149 −149 时就真的表示不出来了。这时返回零。
所以最终函数的实现是这样的:
unsigned floatPower2(int x) {
if (x >= 0)
{
if (x <= 0x80)
{
int ans = (0x7f + x) << 23;
return ans;
}
// 太大了
return 0xff << 23;
}
if (x < 0)
{
// 规格化和非规格化的分界线: x = -126 时,阶码为 1.
// 此时表示的值是 2 的 -126 次方。
if (x >= -126)
{
int ans = (0x7f + x) << 23;
return ans;
}
// 非规格化的值应该怎么办?
if (x >= -149)
{
int ans = 1 << (23 - (-x - 126));
return ans;
}
// 再小就真的表示不出来了
return 0;
}
}
不过因为超时的问题这个函数过不了样例……测试时在 btest 后面加上 “-T 20” 把时间限制放宽一些就能通过了。
这样,csapp 的第一个实验 datalab 就做完了。给自己点个赞吧。