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.
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.
TMin = 1000...0000 in binary, so shift 1 to the left (
0000...0001 in binary) by 31 to get
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
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.
-a = ~a + 1
We are doing comparison in this question. Check question 8 to see how we compare two numbers using only bit-level operations.
The ternary operator
x ? y : z will return
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.
((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
z. Since we have
1111...1111 as our mapped value in bit level representation, we could simply apply an
a AND 0will empty the value and set all bits to 0.
a AND -1will give out exactly the same value as the input.
By doing so, we can set either of
z to zero while leaving one of them unchanged. Finally, we can get the result by adding them up.
There are two cases that
x <= y:
xis negative and
We can determine this by checking the sign bits of
xis signed while
yis not, return
yhave the same sign but
y - xis positive.
We can determine this by checking if the sign bits of
yare the same and checking the sign bit of
y - x. If both
yhave the same sign bit and sign bit of
y - xis 0, return
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.
/* 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 like
0000 0000 1000 1010. We want to fill the lower bits with ones (i.e. turn it into
0000 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.
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
- 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
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
- 8 bits for
- 23 bits for
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.
For this question, we should consider 4 conditions:
- Too small to be represented as denormalized
- 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
Hence, everything with an exponent less than -150 should be regarded as 0, and everything with an exponent greater than 127 should be
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.