The lab deals with writing timing and randomized tests, comparing their outputs and finding and fixing a tiny bug.
Timing Tests for List61B
Timing the construction of an AList with a bad resize strategy
As discussed , a multiplicative resizing strategy will result in fast add operations / good performance, whereas an additive resizing strategy will result in slow add operations / bad performance. In this part of the lab, we’ll show how we can demonstrate this empirically.
In the timingtest package, we’ve provided the AList class created in lecture with the bad resizing strategy below:
Your goal for this part of the lab is to write code that tabulates the amount of time needed to create a AList of various sizes using the addLast method above. The output of this timing test will look something like this:
/**
* Returns a table with rows at N breakpoints from startingN to MAX_N for addLast operation of AList
*/
public static void timeAListConstruction() {
//starting nr of ops
int startingN = Math.multiplyExact(1, 1000);
//max nr of ops
int MAX_N = Math.multiplyExact(128, 1000);
AList<Integer> Ns = new AList<>();
AList<Double> times = new AList<>();
AList<Integer> opCounts = new AList<>();
/**
* for MAX_N = 128 runs 8 times - br points:
* 1 000 / 2 000/ 4 000/ 8 000/ 16 000/ 32 000 /64 000 /128 000
*/
for (int n = startingN; n <= MAX_N; n *= 2) {
AList<Integer>timedA = new AList<>();
Stopwatch sw = new Stopwatch();
int opCount;
/**
* Fill ArrayList timedA with ints from 0 to N
* */
for (opCount = 0; opCount < n; opCount += 1) {
timedA.addLast(opCount);
}
//stop Stopwatch
times.addLast(sw.elapsedTime());
Ns.addLast(n);
opCounts.addLast(opCount);
}
printTimingTable(Ns, times, opCounts);
}
Output - of the timing test - AList with a bad resize strategy:
Timing the construction of an AList with a good resize strategy
Now modify the AList class so that the resize strategy is multiplicative instead of additive and rerun timeAListConstruction. Your AList objects should now be constructed nearly instantly, even for N = 128,000, and each add operation should only take a fraction of a microsecond.
Solution:
AList - addLast - modified resize to be multiplicative:
/** Inserts X into the back of the list. */
public void addLast(Item x) {
if (size == items.length) {
resize(size * 2);
}
items[size] = x;
size = size + 1;
}
Output - of the timing test - AList with a good resize strategy:
Suppose we want to compute the time per operation for getLast for an SLList and want to know how this runtime depends on N. To do this, we need to follow the procedure below:
Create an SLList.
Add N items to the SLList.
Start the timer.
Perform M getLast operations on the SLList.
Check the timer. This gives the total time to complete all M operations.
Solution:
class TimeSLList
public static void timeGetLast() {
//starting nr of ops
int startingN = Math.multiplyExact(1, 1000);
//max nr of ops
int MAX_N = Math.multiplyExact(128, 1000);
int MAX_OP_COUNT = Math.multiplyExact(1, 10000);
AList<Integer> Ns = new AList<>();
AList<Double> times = new AList<>();
AList<Integer> opCounts = new AList<>();
/**
* for MAX_N = 128 runs 8 times - br points:
* 1 000 / 2 000/ 4 000/ 8 000/ 16 000/ 32 000 /64 000 /128 000
* Fills SLList with N items
* Calls getLast() of the SLList MAX_OP_COUNT times
*/
for (int n = startingN; n <= MAX_N; n *= 2) {
SLList<Integer> timedSLL = new SLList<>();
int opCount;
/**
* Fill SLList timedSLL with stuffing (ints from 0 to N)
* */
for (int stuffing = 0; stuffing < n; stuffing += 1) {
timedSLL.addLast(stuffing);
}
Stopwatch sw = new Stopwatch();
// call getLast() MAX_OP_COUNT times
for (opCount = 0; opCount < MAX_OP_COUNT; opCount += 1) {
timedSLL.getLast();
}
//stop Stopwatch
times.addLast(sw.elapsedTime());
Ns.addLast(n);
opCounts.addLast(opCount);
}
printTimingTable(Ns, times, opCounts);
}
Output of the timing test - SLList getLast() - 1000 times for various sizes of SLList:
Obviously not a great output for retrieving the last item - this implementation of SLList goes over all the items to reach the last one.
Randomized Comparison Tests
The goal of the rest of this lab is to find the bug in BuggyAList - a resizing AList. For our comparison, we're using a non-resizing, simple version of AList, called AListNoResizing that has an underlying array with a hardcoded size of 1000 .
Write a test called testThreeAddThreeRemove in TestBuggyAList that adds the same value to both the correct and buggy AList implementations, then checks that the results of three subsequent removeLast calls are the same.
Solution:
testThreeAddThreeRemove
/*
* Simple comparison test
* Test compares the results from performing the same operation on 2 classes:
* Class AListNoResizing vs TestBuggyAList
*/
@Test
public void testThreeAddThreeRemove() {
AListNoResizing<Integer> ALNR = new AListNoResizing<>();
BuggyAList<Integer> BAL = new BuggyAList<>();
int testVal1 = 3;
int testVal2 = 4;
int testVal3 = 5;
ALNR.addLast(testVal1);
ALNR.addLast(testVal2);
ALNR.addLast(testVal3);
BAL.addLast(testVal1);
BAL.addLast(testVal2);
BAL.addLast(testVal3);
assertEquals(ALNR.removeLast(), BAL.removeLast());
assertEquals(ALNR.removeLast(), BAL.removeLast());
assertEquals(ALNR.removeLast(), BAL.removeLast());
}
The test passed - it's not strong enough to identify the bug in BuggyAList
Next we were encouraged to write a randomized test and use the debugger to find the bug, I came up with the following test suite:
Solution:
@Test
public void randomizedTest() {
AListNoResizing<Integer> L = new AListNoResizing<>();
BuggyAList<Integer> BL = new BuggyAList<>();
int N = 500;
for (int i = 0; i < N; i += 1) {
int operationNumber = StdRandom.uniform(0, 4);
// if operation can't be executed proceed to the next iteration
if (L.size() == 0 && SizeNonZeroOperation.get(operationNumber)) {
continue;
}
switch (operationNumber) {
case 0 -> {
int randVal = StdRandom.uniform(0, 100);
L.addLast(randVal);
BL.addLast(randVal);
}
case 1 -> {
int sizeL = L.size();
int sizeBL = BL.size();
assertEquals(sizeL, sizeBL);
}
case 2 -> {
int lastValL = L.getLast();
int lastValBL = BL.getLast();
assertEquals(lastValL, lastValBL);
}
case 3 -> {
int lastValL = L.removeLast();
int lastValBL = BL.removeLast();;
assertEquals(lastValL, lastValBL);
}
default -> System.out.println("Operation nr " + operationNumber + " not implemented.");
}
}
}
Fixing the bug
Actually the bug lied an incorrect argument to the resize method
size != items.length, items is the underlying array, whose length only changes on resize, while size is an attribute that represents the number of elements in the array.