CMU updated their lab questions for CSAPP last year, and many solutions online are actually outdated. I’m trying to figure out these questions and I’ll post my solutions for the latest Data Lab with some explanations here. I hope this could be helpful if you have any problems with the lab questions.
1. bitXor
1 | /* |
For this problem, we need to know that not(not a and not b) = a or b (i.e. ~(~x & ~y) = x | y)
(De Morgan’s laws) and (x or y) and not(x and y) = x xor y
.
2. TMin
1 | /* |
TMin = 1000...0000
in binary, so shift 1 to the left (0000...0001
in binary) by 31 to get TMin
.
3. isTmax
1 | /* |
The first trick is to get the value of TMax by simply flipping the bit-level representation of TMin.
Then, you might notice that the return statement is pretty similar to the one in bitXor
, only with an extra !
(logical negation). Actually, by doing the xor operation, we are checking the equality of two numbers. We could also say that the xor operation works just like !=
, so by adding a !
to our xor implementation, we are literally just checking the equality like what ==
does.
4. allOddBits
1 | /* |
Read the prompt first. It could be helpful.
Then you may start to wonder what is special about 0xAAAAAAAA. Well, you can try to convert it to binary and it turns out to be 0xAAAAAAAA = 0b10101010101010101010101010101010
, which is a number with all odd-numbered bits in the word set to 1, exactly the same thing asked in this problem.
What we are going to do is calculate x & 0xAAAAAAAA
. This operation allows us to ignore all numbers on even-numbered bits. Since the numbers on even-numbered bits are not helpful for this problem and it only makes things more tricky so we should ignore them. By ignoring the even-numbered bits, we will get the same value as 0xAAAAAAAA
if x has all of its odd-numbered bits set to 1.
Finally, check if the result has the same value as 0xAAAAAAAA
using the equality check trick we did before.
5. negate
1 | /* |
-a = ~a + 1
6. isAsciiDigit
1 | /* |
We are doing comparison in this question. Check question 8 to see how we compare two numbers using only bit-level operations.
7. conditional
1 | /* |
The ternary operator x ? y : z
will return y
if x
is true and z
otherwise. In order to map an integer value to boolean, we first need to know that 0
will be converted to false
while any other values will be converted to true
, so we need to somehow create an algorithm to map x
to a new domain.
The expression ((x | (~x + 1)) >> 31)
will convert x
to 0 if x = 0
and convert x
to -1 (1111….1111 in binary) if x != 0
.
Then, we can apply the mapped x
value to y
and z
. Since we have 0000...0000
or 1111...1111
as our mapped value in bit level representation, we could simply apply an AND
operation.
a AND 0
will empty the value and set all bits to 0.a AND -1
will give out exactly the same value as the input.
By doing so, we can set either of y
and z
to zero while leaving one of them unchanged. Finally, we can get the result by adding them up.
8. isLessOrEqual
1 | /* |
There are two cases that x <= y
:
x
is negative andy
is positive
We can determine this by checking the sign bits ofx
andy
. Ifx
is signed whiley
is not, return1
;x
andy
have the same sign buty - x
is positive.
We can determine this by checking if the sign bits ofx
andy
are the same and checking the sign bit ofy - x
. If bothx
andy
have the same sign bit and sign bit ofy - x
is 0, return1
.
9. logicalNeg
1 | /* |
For a logical negation, we want it to return 1 when and only when x is 0. A special property of 0 in 2’s complement is that there’s no negative zero, so (x | -x)
will give out 0 if x is zero, while giving out a number with 1 on its most significant bit when x is nonzero.
Then we shift the value of (x | -x)
by 31 so that we can get -1 if x is nonzero or 0 if x is zero. (Remember that arithmetic shift copies the most significant bit while shifting, which means if we have a number with 1 on its MSB, it will keep copying 1 and give us 111....111
which is -1 in 2’s complement)
Finally, we add a 1 as bias to get the result.
10. howManyBits
1 | /* howManyBits - return the minimum number of bits required to represent x in |
This question is one of the trickiest questions in this lab. It took me quite a long time to figure this out.
We can divide the whole process into three steps.
- Flip the bits if x < 0
In 2’s complement, if we have more bits available than what is required to represent a number, we copy the sign bit until all empty bits are filled with duplicated sign bits, which is called sign extension. By flipping the bits of a negative number, we can remove the leading ones in its bit level representation caused by sign extension. - Fill the lower bits with ones
Now we have a new number with bit level representation like0000 0000 1000 1010
. We want to fill the lower bits with ones (i.e. turn it into0000 0000 1111 1111
) so that we can count the number of ones to get our result. - Count ones
We can count ones by grouping and adding. Each step of those five, adds neighbouring bits together in groups of 1, then 2, then 4 etc. The method is based in divide and conquer.
Check out this post on StackOverflow for detailed explanation on this algorithm: https://stackoverflow.com/questions/3815165/how-to-implement-bitcount-using-only-bitwise-operators
Don’t forget to add a one for sign bit for our final result.
I strongly recommend you to pick a small number (e.g. a 8-bit number) and do the whole process by hand to understand how the algorithm works.
11. floatScale2
1 | /* |
According to the IEEE Standard for floating point number, a single-precision floating point number is stored and formatted like this.
And there’re generally three ways to interpret a floating point
- Normalized
- Denormalized
- Special value
For this question, we first need to determine whether this number is a normalized, denormalized or special value representation by checking the exp
field. We can get the value of exp
field using ((uf >> 23) & 0xff)
(i.e. shift to right by 23 and get rid of the sign bit).
If the number is a 0 or special value representation, return uf
without any modification.
If the number is a denormalized representation, shift the frac
field to left by 1 without changing the exp
field. To get the frac
field, we use (uf & 0x007FFFFF)
to empty all bits but the frac
field. Don’t forget to put the sign bit back.
If the number is a normalized representation, add 1 to the exp
field.
12. floatFloat2Int
1 | /* |
In order to cast a floating point number into integer, we first need to extract the three fields of a floating point representation.
- 1 sign bit
s
- 8 bits for
exp
field - 23 bits for
frac
field
Then we subtract the bias to get the real exponent and add the implicit 1 to frac
field in order to get the real mantissa M
. Although there is a denormalized representation for representing small numbers, but since we are casting to integer and all those small numbers will be rounded to 0, we don’t need to care about the denormalized representation in this case.
Check the exponent. If the exponent is less less than zero (negative exponent) or the exp
field is zero (denorm), return 0 because the result will be rounded to zero anyway.
If the exponent is greater than or equal to 31 (overflow while casting to integer) or the exp
field is equal to 0xFF
(special values), return 0x80000000
according to the instruction in the prompt.
If the exponent is greater than 24, left shift by exponent - 24
.
If the exponent is less than or equal to 24, right shift by 24 - exponent
.
Finally, we check the sign bit an negate our result if the original number is nagetive.
13. floatPower2
1 | /* |
For this question, we should consider 4 conditions:
- Too small to be represented as denormalized
- Denormalized
- Normalized
- Too big
Since we know that we have 1 bit for sign, 8 bits for exp
field and 23 bits for frac
field in a single precision number, we can figure out the range of those four conditions.
In normalized representation, the maximum value for exp
field is 254 (since we cannot have all ones), so the maximum exponent should be 254 - Bias = 254 - 127 = 127
. The minimum value for exp
field is 1 (we cannot have all zeros as well), so the minimum exponent value should be 1 - Bias = 1 - 127 = -126
.
In denormalized representation, the exp
field is always 0 (giving out an exponent of -126) and the powers of two are represented by the frac
field. The minimum value in our case is 2^-150
while the maximum value is 2^-127
.
Hence, everything with an exponent less than -150 should be regarded as 0, and everything with an exponent greater than 127 should be +INF
.
After knowing the range of each representation, we can set up a conditional statement and compose the result.
As we mentioned before, the powers of 2 in denormalized representation are calculated by manipulating the bits in frac
field. Therefore, we can simply shift the 1 in frac
field by correct amount to get the result.
For normalized representation, we change the value of exp
field to get a power of 2, but don’t forget to shift the whole exp
field to the correct position (shift to left by 23). Leave the frac
field empty so that it will be interpreted as 1.0.