cs61c Fa03 mt1 Sol
cs61c Fa03 mt1 Sol
Most of the short-answer questions were worth 1 point or 1/2 point, with no
possibility of partial credit. Scoring information is given below only for
the problems where it's not obvious from the above. We carried half points to
the front of the exam, but after adding the points we truncated the result to
an integer.
1a. Negation.
With N bits you can represent 2^N values, but when you are representing signed
integers about half those values are negative, so the largest magnitude is
around 2^(N-1). For N=8, 2^N=256 and 2^(N-1)=128, so the answer has to be
close to -128.
For eight bits, the range is from -(2^7) to (2^7)-1, or -128 to 127. So the
exact answer is indeed -128.
0xFA25 + 0xB705. The easiest way to do this is the same way you'd do addition
of decimal integers: add from right to left, carrying when the result is 16 or
more.
So the result is 0x1B12A. But we only have 16 bits, which is 4 hex digits, so
the leftmost digit is lost and the result is 0xB12A.
If you didn't know the addition table for hex, another way to solve this
problem is to convert to binary and add the binary values:
carry 1 1111 11 1 1
first number 1111 1010 0010 0101
second number 1011 0111 0000 0101
---------------------
1 1011 0001 0010 1010
Dropping the leftmost bit and converting back to hex gives 0xB12A.
If we'd allowed calculators, you could have converted the numbers to decimal
and added them. Since they're signed numbers and both have the leftmost bit
on, both are negative, so we should take their twos complements to get their
absolute value.
We get the ones complement by subtracting each hex digit from 15 decimal, then
we add 1 (to the entire number, not to each digit) to get the twos complement:
1e. Overflow?
This allocates a six-byte character array (including a byte for the null at
the end) in global data space. Every call to set() refers to this same array.
So each of the three calls changes one character: thing -> thong -> whong ->
wrong. So the result that's printed is "wrong".
This allocates a new six-byte array on the stack for each call, then returns
the address of that stack array, but the stack frame containing it is
deallocated when set() returns. So what will be printed is whatever the call
to printf() puts at that address on the stack! The result is therefore
undefined, or a runtime error.
This heap-allocates a new six-byte array for each call. Each array has the
initial value "thing" and then one character is changed. So the first two
calls have essentially no effect; the third call changes thing -> tring, and
"tring" is printed.
&p1 = &a[0]; Any expression starting with & is a constant, not a variable,
so this is an attempt to assign a value to a constant, like
saying 3 = 4; so it's ILLEGAL.
[Scoring: You started with 2 points, and lost 1/2 point for each error -- each
statement that either should be crossed out but isn't, or shouldn't be but
is -- with a floor of zero points.]
The advantage of the stack over the heap is that allocation and deallocation
are just a single instruction each:
addi $sp, $sp, -framesize # allocate
addi $sp, $sp, framesize # deallocate
By contrast, heap allocation requires searching the free list to find a large
enough free block, removing that block (or part of it) from the free list, and
updating the block header.
During the exam, several people asked "what is the address of LOOP?" But
branch instructions don't contain the target address; they contain the offset
from the current PC (which points just after the branch) to the target
address. In this case, you don't need to know where LOOP is; you can see
that LOOP is the instruction before the branch, and that's all you need.
Perhaps the reason for the confusion is that the *assembly language* branch
instruction gives the target address (in the form of a label), while the
*machine language* instruction gives the offset. The assembler computes the
offset and uses it in the machine instruction that it generates.
$zero = $0 = 00000.
LOOP is the instruction before the branch. The offset would be zero to branch
to the instruction after the branch, -1 to branch to the branch instruction
itself, and -2 to branch to the previous instruction. -2 is 1111111111111110
as 16 bits.
which is 0x1500fffe.
Note that this is not the same as question 2c, which was about stack vs. heap
allocation.
The main reason to keep pointers in registers is that all MIPS memory
references use I-format (register+offset) addressing. So in order to get to
things on the stack, we need a register that contains an address near the
thing we want. The most straightforward way to do this is to keep the address
of the current stack frame in a register. This is answer 2.
The reason why the given ADDI instruction is not an actual machine instruction
is that its operand doesn't fit in 16 bits. So we have to get it into a
register, namely $at, the one reserved for use by the assembler.
The key point to notice here is that fields i and d share memory. Each of the
unions (x and y) are therefore 8 bytes long (the larger of sizeof(int) and
sizeof(double)). sizeof(struct point) is therefore 16.
There was some confusion about the second instruction, which produces a
pointer to a nonexistent array element. The array has 10 elements, namely
p[0] through p[9]. So why do we make a pointer past the end of the array?
Because we aren't going to dereference this pointer; we use it only for the
test for the end of the loop! When $8 reaches the value in $9, we've gone
past the end of the array, so we stop looping.
Scoring: We counted the two uses of $0 as a single answer. Thus there are
four answers here: 160, $0, 8, and 16. Each of these was worth 1/2 point.
Many people had trouble with this question, partly because you didn't read,
or didn't believe, the part about "the same as IEEE." So, during the exam,
we got questions like "is the exponent biased?" and "does this include
denorms?" We answered by saying "the same as IEEE" but of course that
implies "yes" to both questions.
What is the exponent bias? In IEEE single precision, with an 8-bit exponent
field, the bias is 127, which is (2^7)-1. For our format, with a 3-bit
exponent field, the bias will be (2^2)-1, which is 3. An all-zero exponent
field is used for zero and denorms; an all-one exponent field is used for
infinity and NaN, so the range representing normalized numbers is 001-110,
which after bias conversion means -2 to 3.
There are four significand bits, plus (except for denorms) an implicit one
before the binary point.
With 8 bits there are 2^8 = 256 possible values. But 1/8 of those (32 of
them) have exponent 4, and are therefore infinite or NaN, leaving 224. And
two of those, +0 and -0, are equal, so there are 223 distinct numbers exactly
representable in this notation. We also accepted 224, because several people
asked during the exam whether to count +0 and -0 as different. But strictly
speaking the answer has to be 223, because the question asks "how many
numbers," not "how many number representations." And +0 is definitely the
same number as -0!
4b. The significand of a float (not including denorms) always has a value
between 1 and 2. (More precisely, 1 <= sig < 2.) This "fills up" the space
between consecutive powers of 2, as represented by the exponent field. In
other words, the number of exactly representable numbers between any two
consecutive powers of two is a constant. That is, the number of representable
numbers between 1 and 2 is equal to the number of numbers between 2 and 4,
which is equal to the number of numbers between 4 and 8, which is equal to the
number between 8 and 16, etc. The numbers get "thinner" as the magnitude gets
larger.
So 0.002 * 3 * X = 15
Another way to do it, avoiding having to work out the cycle time from the
clock rate, is to convert the fundamental equation above to this form:
Some people asked about the "micro" prefix. You should know these!
pico trillionths p
nano billionths n
micro millionths mu
milli thousandths m
--------------------------
kilo thousands k
mega millions M
giga billions G
tera trillions T
6. Memory allocation.
If all blocks are the same size, we should maintain a free list that contains
only blocks of that exact size. So we're never in the situation in which we
allocate less than an entire free block, so there's no fragmentation.
Similarly, there's no need to coalesce small free blocks to make a big one,
since the size we want is exactly the size we have.
On the other hand, we do still need to maintain a list of free blocks, because
over time they'll be scattered among other (same size) blocks that are in use.
And it's still a good idea to keep the freeing of memory out of the hands of
human beings by using a garbage collection system. (Think about a Lisp system,
in which all pairs are the same size, and they're garbage collected!)
7. MIPS mergesort.
mergesort:
addi $sp, -12 # prologue:
sw $ra, 0($sp) # we accept either order of saving
sw $a0, 4($sp) # the two things we know at start
...
add $a1, $v0, $zero # ret val becomes 2nd arg
lw $a0, 8($sp) # saved result becomes 1st arg
epi: lw $ra, 0($sp) # epilogue:
addi $sp, 12 # restore $ra and stack
j merge # goto merge
In this version our stack frame and merge's stack frame aren't both on the
stack at the same time, so the memory required to run the program doesn't grow
because of the call to merge. This is exactly what a Scheme interpreter or
compiler does to implement tail call elimination.
But you shouldn't try to be clever when taking an exam. Just do the
straightforward thing, as above.
The scoring of this problem was less straightforward than the others.
We wanted a solution with several minor errors, but still basically having
the right structure and many correct details, to get some credit. So we
divided all the errors we found into three categories:
There were too many minor errors to list them all, but here's a representative
sample:
Using saved registers ($s0 - $s7, $16 - $23) without saving and
restoring them.
Not saving and restoring $ra ($31). But note that not saving and
restoring *anything* counts as a disaster.
Not reloading the argument list into $a0 before calling odds().
In the base case test, loading list->next from memory before checking
whether list == NULL. (This leads to a seg fault!)
In the base case test, checking (... && ...) instead of (... || ...).
Typically this means BNEZ NOT_BASE instead of BEQZ BASE.
Losing the return value ($v0, $2) from one call after another call.
B MERGESORT instead of JAL MERGESORT. (We think this means you never
learned that recursion doesn't mean "go back to the beginning" in
61A!)
Huge gaps in the code, e.g., only two procedure calls instead of
five (odds, evens, mergesort twice, merge).
-----------------------------------