CS360 Final Exam. Answer to Question 1

17 points

December 13, 1997

Part 1

Each field in a struct dlist is 4 bytes. Therefore, sizeof(struct dlist) is 12. When you malloc() a struct dlist, malloc() pads the size of the structure to a multiple of eight, and then adds eight bytes for bookkeeping. That's 24 bytes per struct dlist. Therefore, a dlist with 1,000,000 nodes will consume 24,000,000 bytes.

Part 2

If you had a second val field, then sizeof(struct dlist) would be 16, and malloc() would not do any padding. Thus, each dlist node would still consume 24 bytes of memory, and your colleague would be right. If you were on a machine where pointers were 8 bytes, then one dlist node would consume 32 bytes (24 for the struct dlist and 8 bookkeeping). If you added another val field, then one dlist node would consume 40 bytes, so your colleague would be wrong.

Part 3

The idea here is that you don't call malloc() simply once for every dlist node. Instead, you do some buffering of your own. The first time you call make_dl(), you call malloc(sizeof(struct dlist)*1000), and you carve off 12 bytes for the dlist node and return it. You keep track of the other 999 dlist nodes, and you use them for subsequent calls to make_dl() or dl_insert_b(). When all of those nodes have been used up, you call malloc(sizeof(struct dlist)*1000) again.

So, a design question is how to keep track of the dlist nodes that have been malloc()'d, but not used. There are pretty much two ways to do this:

  1. Use a free list.
  2. Keep an array index to the first unused node.
The free list approach is easier and allows you to reuse nodes after you call dl_delete_node(). I'll show how to do both. First, to do it, you need to write a procedure dl_malloc() which returns a Dlist. This is what make_dl() and dl_insert_b() will use instead of malloc(). So, here is the modified code for make_dl() and dl_insert_b()
Dlist make_dl()
{
  Dlist d;

  d = dl_malloc();
  d->flink = d;
  d->blink = d;
  d->val = (void *) 0;
  return d;
} 

dl_insert_b(node, val)  /* Inserts to the end of a list */
Dlist node;
void *val;
{  
  Dlist last_node, new;

  new = dl_malloc();
  new->val = val;

  last_node = node->blink;

  node->blink = new;
  last_node->flink = new;
  new->blink = last_node;
  new->flink = node;
}
Now, the free list approach to dl_malloc() is for you to maintain your own free list of dlist nodes. So, I keep a global variable called dl_free_list, which is initially NULL. Then when dl_malloc() is called, it checks to see if dl_free_list is NULL. If so, it malloc()s 1000 dlist nodes, chains them all together using the flink field of the Dlist struct, and makes dl_free_list the head of the list. Then it simply carves off one node and returns it. The code for this is below:
static Dlist dl_free_list = NULL;

static Dlist dl_malloc()
{
  int i;
  Dlist d;

  if (dl_free_list == NULL) {
    dl_free_list = (Dlist) malloc(sizeof(struct dlist)*1000);
    if (dl_free_list == NULL) { perror("malloc"); exit(1); }
    for (i = 0; i < 999; i++) {
      dl_free_list[i].flink = dl_free_list+(i+1);
    }
    dl_free_list[999].flink = NULL;
  }
  d = dl_free_list;
  dl_free_list = d->flink;
  return d;
}
The nice thing about this approach is that when you call dl_delete_node, you can put the node back onto the free list:
dl_delete_node(item)   
Dlist item;
{ 
  item->flink->blink = item->blink;  /* First unhook the item from the dlist */
  item->blink->flink = item->flink;

  item->flink = dl_free_list;        /* Then put it on the free list */
  dl_free_list = item;
}
That's it.

Now, if you used the array approach, you need two global variables -- an array of dlist nodes, and an index to the first free element of the array. When you call dl_malloc() it simply increments the index and returns the dlist that the old value of the index pointed to. When the index hits 1000, you have to malloc() a new array of dlist structs. Here's the code for this:

static Dlist dl_array;
static int dl_index = 1000;

static Dlist dl_malloc()
{
  Dlist d;

  if (dl_index == 1000) {
    dl_array = (Dlist) malloc(sizeof(struct dlist)*1000);
    if (dl_array == NULL) { perror("malloc"); exit(1); }
    dl_index = 0;
  }
  d = dl_array+index;
  index++;
  return d;
}
The problem with this implementation is that you can't do anything with dl_delete_node unless you add some additional structure. You could make a free list out of these, and then modify dl_malloc() to use the free list first and then the array if the free list is empty. That would be a nice solution to this problem.

Part 4

You're calling malloc(sizeof(struct dlist)*1000) 1000 times. Each time you call malloc(sizeof(struct dlist)*1000), 12*1000+8 = 12,008 bytes are consumed. Therefore, your implementation consumes 12,008,000 bytes.