[Thanks to Saurabh Joshi for some of the suggestions.]
1. [30 points] For each of the memory reference streams given in the
following, compare the cost of executing it on a bus-based SMP that supports
(a) MESI protocol without cache-to-cache sharing, and (b) Dragon protocol.
A read from processor N is denoted by rN while a write from processor N is
denoted by wN. Assume that all caches are empty to start with and that cache
hits take a single cycle, misses requiring upgrade or update take 60 cycles,
and misses requiring whole block transfer take 90 cycles. Assume that all
caches are writeback.
Solution:
Stream1: r1 w1 r1 w1 r2 w2 r2 w2 r3 w3 r3 w3
(a) MESI: read miss, hit, hit, hit, read miss, upgrade, hit, hit, read
miss, upgrade, hit, hit. Total latency = 90+1+1+1+2*(90+60+1+1) = 397 cycles
(b) Dragon: read miss, hit, hit, hit, read miss, update, hit, update,
read miss, update, hit, update. Total latency = 90+1+1+1+2*(90+60+1+60) = 515 cycles
Stream2: r1 r2 r3 w1 w2 w3 r1 r2 r3 w3 w1
(a) MESI: read miss, read miss, read miss, upgrade, readX, readX, read miss,
read miss, hit, upgrade, readX. Total latency =
90+90+90+60+90+90+90+90+1+60+90 = 841 cycles
(b) Dragon: read miss, read miss, read miss, update, update, update, hit, hit,
hit, update, update. Total latency = 90+90+90+60+60+60+1+1+1+60+60=573 cycles
Stream3: r1 r2 r3 r3 w1 w1 w1 w1 w2 w3
(a) MESI: read miss, read miss, read miss, hit, upgrade, hit, hit, hit, readX,
readX. Total latency = 90+90+90+1+60+1+1+1+90+90 = 514 cycles
(b) Dragon: read miss, read miss, read miss, hit, update, update, update,
update, update, update. Total latency=90+90+90+1+60*6=631 cycles
[For each stream for each protocol: 5 points]
2. [15 points] (a) As cache miss latency increases, does an update protocol
become more or less preferable as compared to an invalidation based protocol?
Explain.
Solution: If the system is bandwidth-limited, invalidation protocol will remain the
choice. However, if there is enough bandwidth, with increasing cache miss
latency, invalidation protocol will lose in importance.
(b) In a multi-level cache hierarchy, would you propagate updates all the way
to the first-level cache? What are the alternative design choices?
Solution: If updates are not propagated to L1 caches, on an update the L1 block must be
invalidated/retrieved to the L2 cache.
(c) Why is update-based protocol not a good idea for multiprogramming workloads
running on SMPs?
Solution: Pack-rat. Discussed in class.
3. [20 points] Assuming all variables to be initialized to zero, enumerate all
outcomes possible under sequential consistency for the following code segments.
(a) P1: A=1;
P2: u=A; B=1;
P3: v=B; w=A;
Solution: If u=1 and v=1, then w must be 1. So (u, v, w) = (1, 1, 0) is not allowed.
All other outcomes are possible.
(b) P1: A=1;
P2: u=A; v=B;
P3: B=1;
P4: w=B; x=A;
Solution: Observe that if u sees the new value A, v does not see the new value of B, and
w sees that new value of B, then x cannot see the old value of A. So
(u, v, w, x) = (1, 0, 1, 0) is not allowed. Similarly, if w sees the new
value of B, x sees the old value of A, u sees the new value of A, then v
cannot see the old value B. So (1, 0, 1, 0) is not allowed, which is already
eliminated in the above case. All other 15 combinations are possible.
(c) P1: u=A; A=u+1;
P2: v=A; A=v+1;
Solution: If v=A happens before A=u+1, then the final (u, v, A) = (0, 0, 1).
If v=A happens after A=u+1, then the final (u, v, A) = (0, 1, 2).
Since u and v are symmetric, we will also observe the outcome (1, 0, 2) in
some cases.
(d) P1: fetch-and-inc (A)
P2: fetch-and-inc (A)
Solution: The final value of A is 2.
4. [30 points] Consider a quad SMP using a MESI protocol (without
cache-to-cache sharing). Each processor tries to acquire a test-and-set lock
to gain access to a null critical section. Assume that test-and-set
instructions always go on the bus and they take the same time as the normal
read transactions. The initial condition is such that processor 1 has the lock
and processors 2, 3, 4 are spinning on their caches waiting for the lock to be
released. Every processor gets the lock once, unlocks, and then exits the
program. Consider the bus transactions related to the lock/unlock operations
only.
(a) What is the least number of transactions executed to get from the initial
to the final state? [10 points]
Solution: 1 unlocks, 2 locks, 2 unlocks (no transaction), 3 locks, 3 unlocks (no
transaction), 4 locks, 4 unlocks (no transaction). Notice that in the best
possible scenario, the timings will be such that when someone is in the
critical section no one will even attempt a test-and-set. So when the lock
holder unlocks, the cache block will still be in its cache in M state.
(b) What is the worst-case number of transactions? [5 points]
Solution: Unbounded. While someone is holding the lock, other contending processors may
keep on invalidating each other indefinite number of times.
(c) Answer the above two questions if the protocol is changed to Dragon. [15 points]
Solution: Observe that it is an order of magnitude more difficult to implement shared
test-and-set locks (LL/SC-based locks are easier to implement) in a machine
running an update-based protocol. In a straightforward implementation, on an
unlock everyone will update the value in cache and then will try to do
test-and-set. Observe that the processor which wins the bus and puts its
update first, will be the one to enter the critical section. Others will
observe the update on the bus and must abort their test-and-set attempts.
While someone is in the critical section, nothing stops the other contending
processors from trying test-and-set (notice the difference with
test-and-test-and-set). However, these processors will not succeed in
getting entry to the critical section until the unlock happens.
Least number is still 7. A test-and-set or an unlock involves putting an
update on the bus.
Worst case is still unbounded.
5. [30 points] Answer the above question for a test-and-test-and-set lock for a
16-processor SMP. The initial condition is such that the lock is released and
no one has got the lock yet.
Solution:MESI:
Best case analysis: 1 locks, 1 unlocks, 2 locks, 2 unlocks, ... This involves
exactly 16 transactions (unlocks will not generate any transaction in the best
case timing).
Worst case analysis: Done in the class. The first round will have (16 + 15 + 1
+ 15) transactions. The second round will have (15 + 14 + 1 + 14)
transactions. The last but one round will have (2 + 1 + 1 + 1) transactions
and the last round will have one transaction (just locking of the last
processor). The last unlock will not generate any transaction. If you add these
up, you will get (1.5P+2)(P-1) + 1. For P=16, this is 391.
Dragon:
Best case analysis: Now both unlocks and locks will generate updates. So the
total number of transactions would be 32.
Worst case analysis: The test & set attempts in each round will generate
updates. The unlocks will also generate updates. Everything else will be
cache hits. So the number of transactions is (16+1)+(15+1)+...+(1+1) = 152.
6. [10 points] If the lock variable is not allowed to be cached, how will the
traffic of a test-and-set lock compare against that of a test-and-test-and-set
lock?
Solution:In the worst case both would be unbounded.
7. [15 points] You are given a bus-based shared memory machine. Assume that
the processors have a cache block size of 32 bytes and A is an array of
integers (four bytes each). You want to parallelize the following loop.
for(i=0; i<17; i++) {
for (j=0; j<256; j++) {
A[j] = do_something(A[j]);
}
}
(a) Under what conditions would it be better to use a dynamically scheduled
loop?
Solution: If runtime of do_something varies a lot depending on its argument value or
if nothing is known about do_something.
(b) Under what conditions would it be better to use a statically scheduled
loop?
Solution: If runtime of do_something is roughly independent of its argument value.
(c) For a dynamically scheduled inner loop, how many iterations should a
processor pick each time?
Solution: Multiple of 8 integers (one cache block is eight integers).
8. [20 points] The following barrier implementation is wrong. Make as little
change as possible to correct it.
struct bar_struct {
LOCKDEC(lock);
int count; // Initialized to zero
int releasing; // Initialized to zero
} bar;
void BARRIER (int P) {
LOCK(bar.lock);
bar.count++;
if (bar.count == P) {
bar.releasing = 1;
bar.count--;
}
else {
UNLOCK(bar.lock);
while (!bar.releasing);
LOCK(bar.lock);
bar.count--;
if (bar.count == 0) {
bar.releasing = 0;
}
}
UNLOCK(bar.lock);
}
Solution: There are too many problems with this implementation. I will not list them
here. The correct barrier code is given below which requires addition of one
line of code. Notice that the releasing variable nicely captures the notion
of sense reversal.
void BARRIER (int P) {
while (bar.releasing); // New addition
LOCK(bar.lock);
bar.count++;
if (bar.count == P) {
bar.releasing = 1;
bar.count--;
}
else {
UNLOCK(bar.lock);
while (!bar.releasing);
LOCK(bar.lock);
bar.count--;
if (bar.count == 0) {
bar.releasing = 0;
}
}
UNLOCK(bar.lock);
}