<<

CS107 Cynthia Lee Winter 2017 CS107 Final Exam (Practice #1) You have 3 hours to complete all problems. You don’t need to #include any libraries, and you needn’t use assert to guard against any errors. Understand that the majority of points are awarded for concepts taught in CS107, and not prior classes. You don’t get many points for for-loop syntax, but you certainly get points for proper use of &, *, and the low-level functions introduced in the course.

Problem Points Score Grader 1. Number 5 representation 2. Generics, pointers, and 10 CVector 3. Assembly and 17 optimizations

4. Build 10

5. Heap allocator 18

6. design 6

TOTAL 66

Problem 1: Number Representation (5pts) (a) (2pts) Write the representation of the value -5/8. Recall that minifloat is a (made-up) 8- floating point format with 1 sign bit, 4 exponent , and 3 mantissa bits. Write your answer as a bit pattern in these boxes:

fraction =

Scratch work space:

(b) (1pt) What is the binary equivalent of the following number? 0xCAFE0 (Write your answer in the box.)

(c) (2pt) What does the following code print? (Write your answer in the box.)

unsigned int moana = 0xFFFFFFCC; int maui = (int)moana; printf("%d", maui);

Problem 2: Generics, pointers, and CVector (10 points) (a) (4pts) You would like to use a CVector to hold your CS107 assignment grade . Each entry in the CVector will be a struct that holds information for one assignment. The struct is defined below, and we also show an excerpt of code that calls cvec_create:

typedef struct { int num; // assignment number, e.g., 4 for assign4 double score; // your score as pct, e,g., 0.92 for 46/50 char *letter_grade;// your letter grade as a string, e.g., "A-" } assignT;

// excerpt of code where cvec_create is called CVector *grades = cvec_create(sizeof(assignT), 8, assign_cleanup); // end excerpt

Write a callback function that can be passed to cvec_create that will appropriately clean up memory for your grades vector so there will be no leaks. Assume that any memory associated that could possibly need freeing in this callback should be freed in this callback. In particular, assume the letter grade strings were created using strdup.

void assign_cleanup(void *assign) {

}

[For parts (b), (c)] Recall our generic “swap” function from class (reproduced below). It is used to make two values trade places in memory, and is commonly used in sorting arrays. There’s a right way to call this swap function in normal circumstances, but we’re asking you to use it a bit “creatively” to achieve particular results. Note: what matters for the correctness of these results is that the output if you ran this “for (int i = 0; i < 5; i++) printf("%d %d ", ptr1[i], ptr2[i]);” after the call to swap_any matches what it would be for the “after” diagram shown.

void swap_any(void *a, void *b, size_t sz) { char tmp[sz]; memcpy(tmp, a, sz); memcpy(a, b, sz); memcpy(b, tmp, sz); }

(b) (3pts) Complete the mixup1 function to create this before & after result. Your solution must consist of ONLY completing the arguments of the one call to swap_any, as shown.

Before: After: ptr1 ptr2 ptr1 ptr2 void mixup1() {

int *ptr1 = malloc(5 * sizeof(int)); int *ptr2 = malloc(5 * sizeof(int)); 1 6 8 6 for (int i = 1; i <= 5; i++) { ptr1[i-1] = i; 2 7 9 7 ptr2[i-1] = i + 5; 8 10 3 1 } 4 9 4 2 swap_any(______, 5 10 5 3 ______,

______); }

(c) (3pts) Complete the mixup2 function to create this before & after result. Your solution must consist of ONLY completing the arguments of the one call to swap_any, as shown. In this case (part (c)), the third argument should not be edited other than to specify a single argument (that should be a type) to sizeof().

Before: After: ptr1 ptr2 ptr1 ptr2

void mixup2() {

int *ptr1 = malloc(5 * sizeof(int)); int *ptr2 = malloc(5 * sizeof(int)); for (int i = 1; i <= 5; i++) { 1 6 6 1 ptr1[i-1] = i; 2 7 7 2 ptr2[i-1] = i + 5; 3 8 8 3 } 4 9 9 4 swap_any(______, 5 10 10 5 ______,

sizeof(______)); }

Problem 3: Assembly and optimizations (17 points) Consider the following -64 code output by gcc using the settings we use for this class (-Og):

: mov (%rdi),%eax lea (%rax,%rax,2),%esi add %esi,%esi mov $0x0,%ecx imul $0x31,%esi jmp L1 L3: lea (%rcx,%rax,1),%edx movslq %edx,%rdx mov %esi,(%rdi,%rdx,4) add $0x2,%eax jmp L2 L4: mov %ecx,%eax L2: cmp $0x9,%eax jle L3 add $0x3,%ecx L1: cmp $0x9,%ecx jle L4 mov $0xa,%eax retq

Refer back to this code to answer the questions in parts (a)-(d), on the following pages.

(a) (8pts) Fill in the C code below so that it is consistent with the above x86-64 code. Your C code should fit the blanks as shown, so do not try to squeeze in additional lines or otherwise circumvent this (this may mean slightly adjusting the syntax or style of your initial decoding guess to an equivalent version that fits). Your C code should not include any casting. Note that with the compiler to –Og, some optimization has been performed. One thing you’ll notice right away is that gcc chose not to create an actual eliza array, but instead kept track of its values in other ways. We will ask about optimizations in more detail in later parts of this question. int ham(int *burr) { int eliza[4]; eliza[0] = 7; eliza[1] = 7; eliza[2] = 1;

eliza[3] = ______* burr[0]; // part (b)

for (int i = 0; i < ______; i+=______) {

for (int j = ______; j < ______; j+=______) {

burr[______] = eliza[0] * eliza[1] * eliza[2] * eliza[3]; //(c) } }

if (eliza[0] > eliza[1]) { // part (d) return 8; }

if (burr[0] < burr[1] && burr[0] > burr[1]) { // part (d) return 9; }

return ______; }

Here is a list of optimizations that we discussed in class. Use it to answer parts (b), (c). i. Constant folding ii. Common subexpression elimination iii. Dead code iv. Strength reduction v. Code motion vi. Tail recursion vii. Loop unrolling

(b) (2pts) Refer back to the C code, on the line marked for part (b). It reads: eliza[3] = … * burr[0];

Which optimization from the list most closely relates to this implementation? ______Name and explain the instruction(s) that implement this product, and explain why gcc would choose to do it that way.

(c) (3pts) Refer back to the C code, on the line marked for part (c). It reads: burr[…] = eliza[0] * eliza[1] * eliza[2] * eliza[3];

Which two optimizations from the list most closely relate to when and how this product is calculated? ______Name and explain the instruction(s) that calculate this product off all the eliza terms:

(d) (2pts) Refer back to the C code, on the two lines marked for part (d). (Two if statements.) Neither if statement appears in the assembly code at all, because they are “dead code.” However, the reasons they are considered dead code are slightly different in each case. Explain each case below:  How does gcc know that the first if statement is dead code?

 How does gcc know that the second if statement is dead code?

(e) (1pt) In class, we looked at the following simple array sum function, where the order of summing the elements of the array is dictated by the provided indexes array (it contains some permutation of the numbers [0, ..., n-1]): static int sum_fwd_ind(int a[], int n, int indexes[]) { int sum = 0; for (int i = 0; i < n; i++) sum += a[indexes[i]]; return sum; } We saw in class that, for large arrays, this function performs about 3 times faster when indexes is sorted in ascending order than when indexes is in random order. Explain why this can happen even though the code is the same.

Problem 4: Build process (10 points) For this problem, you are presented with five small programs, along with the commands that in theory can be used to build and execute them. You should write something about each of the six programs, as follows:

 Assume we compile using: gcc –Wall –o prog prog.c, and attempt to run the prog executable (if one was generated) with three arguments, like this: “arg1 arg2 arg3”.  If any warnings or errors are issued during the build process, you should identify which component(s) issue the warning(s) and/or error(s) by circling all applicable responses.  If everything builds (with or without warning(s)) say what will happen when it is executed by circling a response.  Remember that if any step gives an error, subsequent steps do not occur (so do not circle them for warnings/errors), and an executable is not produced (so circle “N/A” for the executable part).  There is no need to provide an explanation, just circle.

The programs aren’t intended to do anything meaningful, as they’re contrived to exercise your understanding of the build process and the C runtime. Note that when #include directives you might expect are NOT used in a program, that’s quite intentional!

(a) (2pts) Consider the following program: int strcmp(const char *s1, const char *s2); int main(int argc, const char *argv[]) { return strcmp(argv[0], argv[1]); } Which component(s) complain with warning(s) and/or error(s)? (circle ALL that apply) Preprocessor Compiler Linker N/A (no warnings or errors)

If an executable is produced, what happens when you run it? (circle ONE) NO crash MAY crash SURE crash N/A (earlier error)

(b) (2pts) Consider the following program: #define PURPLE_RAIN return int main(int argc, const char *argv[]) { if (strlen(argv[0]) > strlen(argv[3])) PURPLE_RAIN argc; else PURPLE_RAIN argc + 1; } Which component(s) complain with warning(s) and/or error(s)? (circle ALL that apply) Preprocessor Compiler Linker N/A (no warnings or errors)

If an executable is produced, what happens when you run it? (circle ONE) NO crash MAY crash SURE crash N/A (earlier error)

(c) (2pts) Consider the following program: #include #include int main(int argc, char *argv[]) { char * tamatoa = "I was a drab little crab once."; tamatoa[8] = 'c'; printf("%s", tamatoa); return 0; } Which component(s) complain with warning(s) and/or error(s)? (circle ALL that apply) Preprocessor Compiler Linker N/A (no warnings or errors)

If an executable is produced, what happens when you run it? (circle ONE) NO crash MAY crash SURE crash N/A (earlier error)

(d) (2pts) Consider the following program: #include #define QUEEN "beyonce"; int main(int argc, const char *argv[]) { printf(QUEEN); return 0; } Which component(s) complain with warning(s) and/or error(s)? (circle ALL that apply) Preprocessor Compiler Linker N/A (no warnings or errors)

If an executable is produced, what happens when you run it? (circle ONE) NO crash MAY crash SURE crash N/A (earlier error)

(e) (2pts) Consider the following program: int prince(int artist); int main(int argc, const char *argv[]) { return prince(argc); } int prince() { return 1999; } Which component(s) complain with warning(s) and/or error(s)? (circle ALL that apply) Preprocessor Compiler Linker N/A (no warnings or errors)

If an executable is produced, what happens when you run it? (circle ONE) NO crash MAY crash SURE crash N/A (earlier error)

Problem 5: Heap allocator (18 points) Consider a heap allocator implementation designed as follows:  Each ’s size (in ) must be a , and the smallest possible block size is 16 bytes (where up to 14 bytes of that is payload).  All blocks, allocated and free, have a pre-node header (1 ), a post-node footer (1 byte), and payload. All three components comprise the block and the total number of bytes of the three components together must be a power of two.  The pre-node header and post-node footer are each 1 byte and have the same format: the least significant bit is an “allocated bit” (1=allocated, 0=free), and the remaining 7 bits indicate the size of the block as the log (base 2) of the number of bytes.  There is a single explicit free list implemented as a sorted linked list of free blocks.  Free nodes have an additional field overlaying the last 8 bytes of the payload. It is a pointer to the header of the next free block, or NULL if the end of the list.  Example 1: If you examine one allocated block that was used to service a malloc of 20 bytes, you would use a block of size 32 bytes: 1 byte header, 20 bytes payload, 10 bytes of unused/waste space, and 1 byte footer. The header and footer bits would be “00001011” (101=5 and 25 = 32 byte block size, followed by a 1 to indicate it is allocated and not free).  Example 2: If you examine one free block of size 32 bytes, you will find (in this order): 1 byte header, 22 bytes free payload, 8 bytes of a pointer to the next free block (or NULL if the end of the list), and 1 byte footer. The header and footer bits would be “00001010” (101=5 for the 32 byte block size, followed by a 0 to indicate it is free).  You do not need to worry about alignment of blocks returned from malloc.

Assume the following global typedefs, constants, and variables have already been set up:

typedef unsigned char header; typedef header footer; static void *heapStart; /* base address of entire heap segment */ static size_t heapSize; /* number of bytes in heap segment */ void *free_list; /* front of the free list */

(a) (2pts) Write a helper function that, given a pointer to a header (or footer), reads and returns the block size of that block, in bytes (should return the total block size, including header and footer bytes, as explained above).

size_t get_size(void *curr) {

}

(b) (2pts) Write a helper function that will help you keep your free list sorted. It takes pointer

to the header byte of a block, and it returns the log2 of the size of the next block (NOT the curr block). This may seem awkward, but it helps with linked list insert to identify when the location of insert is after the current node. If the current node is the end of the list, return 0. unsigned char following_block_size(void *curr) {

}

(c) (2pts) Write a helper function to identify when a block is allocated. Given a pointer to a header byte (or, equivalently, a footer byte, because they have the same format), return true if the block is allocated, otherwise return false. For full credit, this should be fast!— no more than one arithmetic/logic/bitwise operation allowed. bool is_allocated(void *curr) {

}

(d) (4pts) Write two helper functions that will assist in navigating your heap. Given the address of the current block (i.e., the address of the header byte of the current block), these helpers will return the address of the header byte of blocks to the left and right, respectively. If there is none (i.e., at the boundaries of the heap), return NULL. void *left_block(void *curr) {

} void *right_block(void *curr) {

}

(e) (8pts) This helper function takes a pointer to a block and, if the block to its left is a good candidate for merging, merges the current block and the block to its left to form a larger block, with all fields in the new header and footer set appropriately. This function is not responsible for removing coalesced blocks from the free list, nor adding the new block to a free list. In this design, adjacent blocks can only be coalesced if they are free, adjacent, and also the same size as each other (you don’t need to write your answer, but consider why same size is required), so you should check that the adjacent block to the left exists, that it is the same size as the current block, and both are free. If the two blocks were successfully merged, return a pointer to the new block, otherwise return NULL. void *coalesce_left(void *curr) {

}

Problem 6: Cache design (6 points) Consider the following direct-mapped cache layout and contents (note that the used in this problem is a special hypothetical size, not our usual 64-bit pointer size): Line V D Tag (4 Bytes) 0 1 0 111 17 1 1 0 011 9 2 0 0 101 15 3 1 1 001 8 4 1 0 011 4 5 0 0 111 6 6 0 0 101 32 7 1 0 110 3

(a) (2pts) Q: Assuming this cache design follows the principles outlined in class, what can we infer is the size of a memory address in this system?

A: Memory addresses are ______bits.

(b) (2pts) If we attempt a read of memory address 68, will it be a cache hit or a cache miss? Note: 68 is the decimal value of the memory address—you should convert this to bits to help you solve this problem.

Circle one answer: HIT MISS

(c) (2pts) What is the byte offset for memory address 68? (again, 68 is a decimal value) Give your answer in bits.

The byte offset is: ______

Reference

CVector Functions typedef int (*CompareFn)(const void *addr1, const void *addr2); typedef void (*CleanupElemFn)(void *addr); typedef struct CVectorImplementation CVector; CVector *cvec_create(int elemsz, int capacity_hint, CleanupElemFn fn); void cvec_dispose(CVector *cv); int cvec_count(const CVector *cv); void *cvec_nth(const CVector *cv, int index); void cvec_insert(CVector *cv, const void *addr, int index); void cvec_append(CVector *cv, const void *addr); void cvec_replace(CVector *cv, const void *addr, int index); void cvec_remove(CVector *cv, int index); int cvec_search(const CVector *cv, const void *key, CompareFn cmp, int start, bool sorted); void cvec_sort(CVector *cv, CompareFn cmp); void *cvec_first(CVector *cv); void *cvec_next(CVector *cv, void *prev);

Other Functions void *memcpy(void *dest, const void *src, size_t n); void *memmove(void *dest, const void *src, size_t n); void *memset(void *ptr, int value, size_t num); void *malloc(size_t size); void *realloc(void *ptr, size_t size); void free(void *ptr); size_t strlen(const char *s); char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); char *strdup(const char *s); char *strcat(char *dest, const char *src); char *strchr(char *str, int ); char *strtok(char *str, const char *delimiters ); int sprintf ( char * str, const char * format, ... ); int strcmp(const char *str1, const char *str2 ); int strncmp(const char *str1, const char *str2, size_t n);