This lab should be done with your lab partner.
Lab 5 Goals:
- practice with 2D arrays and file I/O in C
- more practice with pointers, dynamic memory allocation, and pass by reference
- timing parts of a program's execution
- writing a Makefile from scratch
- gain expertise in gdb and valgrind for debugging C programs
Lab 5 Introduction
First, both you and your partner should run update31 to
grab some starting point code.
$ update31
$ cd cs31/labs/05
$ pwd
/home/your_user_name/cs31/labs/05
$ ls
Makefile gol.c oscillator.txt
The starting point code includes: an empty Makefile that you need to implement;
gol.c into which you should implement your solution;
and osicllator.txt, a sample input file to your program.
You should create other input files to test your solution.
Project Details and Requirements
Game of Life
For this lab, you will implement a program that plays Conway's Game of Life.
Conway's Game of Life is an example of discrete event simulation, where
a world of entities live, die, or are born based based
on their surrounding neighbors. Each time step simulates another
round of living or dying.
Your world is represented by a 2-D array of values (0 or 1). If a
grid cell's value is 1, it represents a live object, if it is 0 a dead
object. At each discrete time step, every cell in the 2-D grid gets a
new value based on the current value of its eight neighbors:
- A live cell with zero or one live neighbors dies from loneliness.
- A live cell with four or more live neighbors dies due to overpopulation.
- A dead cell with exactly three live neighbors becomes alive.
Your 2-D world should be a TORUS; every cell in the grid
has exactly eight neighbors. In the torus world, cells on
the edge of the grid have neighbors that wrap around to the opposite edge.
For example, the grid locations marked with an 'x' are
the eight neighbors of the grid cell whose value is shown as 1.
x 1 x 0 0 0 0
x x x 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
x x x 0 0 0 0
Conway's Game of Life description from Wikipedia, shows some
example patterns you can use to test the correctness of your
solution (like Blinker, Toad or Beacon).
Implementation Details
Your program will take two command line arguments. The first is a
the name of a configuration file that will specify how to initialize
the game playing variables (dimensions of the grid, number of iterations,
how to initialize the grid cells). The second will indicate whether or
not the game of life board should be printed out after
every iteration or not.
Here are some example command lines:
# run with config values read from file1.txt and do not print the board:
./gol file1.txt 0
# run with config file file2.txt and print the board after each step:
./gol file2.txt 1
Your program should handle badly formed command lines
(e.g. print out an error message and exit).
File Format
The input file format consists of several lines of an ascii file.
The first three lines give the dimensions and
number of iterations, the fourth line lists the number of coordinate pairs
followed by the lines with i j (row index, column index) coordinate
values specifying grid cells that should be initialized to 1
(all others should be 0):
num rows
num cols
num iterations
num of following coordinate pairs (set each (i, j) value to 1
i j
i j
...
You can create your own input files in vim (or emacs) by following the
file input format.
For example, a file with the following contents generates an
interesting pattern that starts in the lower left and walk up to
upper right of grid:
30
30
100
5
29 1
28 2
27 0
27 1
27 2
In addition, you will add timing code to your program to time just
the GOL computation (the timing should not including the board
initialization phase of your code).
Computing Values at Each time step
One problem you will need to solve is how to update each grid cell
value at each time step. Because each grid cell's value is based on
its neighbor's current value, you cannot update each cell's new value
in place (otherwise its neighbors will read the new value and not the
current value in computing their new value).
Requirements
- You must use a Makefile to build your program, and you will write
one from scratch for this assignment. Use Makefiles from other assignments
as a guide. You should use only the following flags to gcc (don't
use the -m32 flag for this assignment):
-g -Wall
Your Makefile should have rules for make and make clean
commands. Use Makefiles that I have given you as a guide. Here is
some basics on writing makefiles
- The program should take two command line arguments: the config file; and either
1 or 0 to enable or disable printing the board after each step.
- When run with 1 as the second command line option should print out the
board after each time step. Use a call to usleep to pause after
each time step so the user can "see" the computation better. The only
calls to usleep should be in the print function(s); running
with 0 as the second command line option option should
include no output of the game board nor any calls to sleep functions
during the run.
- The space for the GOL board(s) should be dynamically allocated in the heap.
See the inclass example from this week as well as
Arrays in C for more
information.
Use the option that does a single malloc of
a contiguous chunk of N*M int (or char). Do not use the int** (or char**) for this assignment.
- You should not use global variables. Instead, the board(s) and
other variables should be local varibles that are passed to
functions that need them. If a function changes the the value
of an argument, remember you need to pass that argument
by reference.
For array parameters, the caller can modify what is in the bucket
values (passing an array is passing its base address to the
function).
- Your program should have timing code in your solution around the GOL main
computation (don't include grid initialization). And print out
the total time for the GOL simulation at the end of the simulation.
Use gettimeofday.
- Use the C FILE interface (fopen, etc.) for file I/O.
- You should detect and handle all errors, you can call exit(1) if the
error is unrecoverable (after printing out an error message of course).
This means that any time you call a function that returns a value, there
should be a check for error return values and they should be handled: do
not just assume that every function call and every system call is always
successful.
- Your solution must have at least three functions in addition to main, and
likley should have more than this. Think good top-down design, and think about some of
the big steps: initializing the game board; updating one round of play;
printing the game board; ... main should represent the high-level steps
with calls to high-level functions that perform each of the big steps.
These functions in turn should use helper functions to implement well-defined
pieces of functionality. As a rule, a function should not be longer than
a page. There are exceptions, but if you are writing a function that is
getting really long, break it up into several parts, each implemented by
a separate function.
- You code should be well-commented code and use good modular design.
See my C documentation
for C references and a C code style guide.
- Your solution should be correct, robust, and free of valgrind errors
Useful C functions and Hints
- Look at the in-class examples from week 8 to see examples of file I/O
and of dynamically allocated 2D arrays:
update31
cd ~/cs31/inclass/08/
- Implement and test incrementally! Use valgrind as you go to catch
memory access errors as you make them.
- Review pointers in C. Here are some
C programming references. In particular, my documentation about pointers in C (and
other C pointer documentation) may be helpful for this assignment.
- Remember C-style pass by reference to "return" more than one value
from a function.
- Remember that a function can return a pointer (this may be useful
if you have an initialization function that returns a pointer to the
malloc'ed and initialized game board.
int *init_board( ...
Functions that return pointer values, generally return NULL on an error.
The caller then will check the return value and decide how to handle a
NULL return value.
- man pages
for C library functions and system calls, and be careful about who is
responsible for allocating and freeing memory space passed to (and returned
by) these routines.
- Here is some more information about
Files in C: I'd recommend using fscanf to read in values
from the file.
- usleep: pauses the program for some unber of micro seconds.
This is useful in print mode to slow down your program after each step so
that you can see what it is doing. About .2 is a good amount to sleep:
usleep(200000); // sleep for 200,000 micro seconds (.2 seconds)
- system("clear"); clears the xterm window so that the next
output is at the top of the window (useful for printing the grid at each
timestep to the same window location).
- gettimeofday can be used to instrument your program with
timers. Call this function before and after the part of the code you want
to time and subtract to get total time.
1 second is 1000000 micro seconds. gettimeofday takes a struct timeval
by reference, the second argument value should be NULL:
struct timeval start_time;
...
ret = gettimeofday(&start_time, NULL);
See the man page (man gettimeofday for more
information.
- atoi converts a string to an integer. For example,
int x = atoi("1234"), assigns x the int value 1234. This is
useful in your command line parsing functions for converting command line
arguments that are read in as strings to their numeric values. See the
man page for atoi for other similar functions to convert stings to other
numeric types (man atoi).
- Remember CNTRL-C will kill your program if it seems to be stuck
in an infinte loop.
Example Output
Here is an example of what you might see from different runs. I'm
printing '@' for 1's and '-' for 0's because it is easier to see
than '0' and '1' characters. You do not need to use the same characters
as me. The
first shows just the start and end board configuration from a run
that is initialized to a very simple oscillator pattern, and the
second is the same configuration, but run with 0 as the second
parameter (notice the time difference between the two):
# a run with output:
$ ./gol oscillator.txt 1
start board:
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - @ @ @ - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
end board:
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - @ - - - - -
- - - - - @ - - - - -
- - - - - @ - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
- - - - - - - - - - -
total time for 21 iterations of 11x11 is 5.493663 secs
# a run with 0 as the second parameter should print no ouput
# the total time then measures just the gol computation part because
# the printfs and usleeps should not be executed when passed 0
$ ./gol oscillator.txt 0
total time for 21 iterations of 11x11 is 1.001186 secs
Extra Challenge
After completing a well-designed, well-commented,
correct solution to the required parts of this assignment, as an
extra challenge you can see how your program compares to mine
speed-wise, and see if you can tune your program to beat mine:
Speed Challenge.
Even if you do not do the extra challange, you may want to read it
over to learn a bit more about running timed experiments, and about
gcc compiler optimization flags.
Submit
Once you are satisfied with your solution, hand it in by typing
handin31 at the unix prompt.
Only one of you or your partner should run handin31 to submit your
joint solutions If you accidentally both run it, send me email right
away letting me know which of the two solutions I should keep and which
I should discard (you don't want the grader to just guess which joint
solution to grade).
You may run
handin31
as many times as you like, and only the
most recent submission will be recorded. This is useful if you realize,
after handing in some programs, that you'd like to make a few more
changes to them.