Notes on Data Structures and Programming Techniques (CPSC 223, Spring 2018) James Aspnes

1 Course administration

1.1 Overview

This is the course information for CPSC 223: Data Structures and Programming Techniques for the Spring 2015 semester. This document is available in two formats, both of which should contain the same information:

Code examples can be downloaded from links in the text, or can be found in the examples directory.

The links above point to www.cs.yale.edu . In case this machine is down, a backup copy of these files can be found at https://www.dropbox.com/sh/omg9qcxkxeiam2o/AACRAJOTj8af6V7RC1cXBHjQa?dl=0.

This document is a work in progress, and is likely to change frequently as the semester progresses.

1.1.1 License

Copyright © 2001–2018 by James Aspnes. Distributed under a Creative Commons Attribution-ShareAlike 4.0 International license: https://creativecommons.org/licenses/by-sa/4.0/.

1.1.2 Resources

1.1.3 Documentation

Please feel free to send questions or comments on the class or anything connected to it to james.aspnes@gmail.com.

For questions about individual assignments, you may be able to get a faster response using Piazza. Note that questions you ask there are visible to other students if not specifically marked private, so be careful about broadcasting your draft solutions.

1.2 Lecture schedule

K&R refers to the Kernighan and Ritchie book. Examples from lecture can be found in the examples directory under 2018/lecture if the links below have not been updated yet.

1.3 Syllabus

Computer Science 223b, Data Structures and Programming Techniques.

Instructor: James Aspnes.

1.3.1 On-line course information

On-line information about the course, including the lecture schedule, lecture notes, and information about assignments, can be found at http://www.cs.yale.edu/homes/aspnes/classes/223/notes.html. This document will be updated frequently during the semester, and is also available in PDF format.

1.3.2 Meeting times

Lectures are MW 13:00–14:15 in WLH 201. The lecture schedule can be found in the course notes. A calendar is also available.

1.3.3 Synopsis of the course

Topics include programming in C; data structures (arrays, stacks, queues, lists, trees, heaps, graphs); sorting and searching; storage allocation and management; data abstraction; programming style; testing and debugging; writing efficient programs.

1.3.4 Prerequisites

CPSC 201, or equivalent background. See me if you aren’t sure.

1.3.5 Textbook

The textbook for this course is:

The C Programming Language (2nd Edition), by Brian W. Kernighan and Dennis M. Ritchie. Prentice Hall, 1988. ISBN 0131103628. The definitive introduction to C. You should memorize this book.

If you are on the Yale campus or are using VPN to get to Yale’s network, you can access this book at http://proquest.safaribooksonline.com/book/programming/c/9780133086249. You do not need to buy a physical copy of this book unless you want to.

1.3.6 Course requirements

Eight weekly homework assignments, and two in-class exams. Assignments will be weighted equally in computing the final grade, and will together count for 60% of the total grade. Each exam will count for 20%.

1.3.7 Staff

See the calendar for open office hours.

You can ask questions that will be seen by the entire staff using the external site Piazza. You can also spam everybody on the course staff using the email address cs223-staff@cs.yale.edu.

1.3.7.1 Instructor

James Aspnes (james.aspnes@gmail.com, http://www.cs.yale.edu/homes/aspnes/). Office: AKW 401. If my open office hours don’t work for you, please send email to make an appointment.

1.3.7.2 Teaching Fellow

Lucas Paul lucas.paul@yale.edu.

1.3.7.3 Undergraduate Learning Assistants

Hannah Block hannah.block@yale.edu.

Elizabeth Brooks elizabeth.brooks@yale.edu.

Yichao Cheng yichao.cheng@yale.edu.

Xiu Chen xiu.chen@yale.edu.

Melina Delgado melina.delgado@yale.edu.

Joyce Duan joyce.duan@yale.edu.

Adriana Elwood adriana.elwood@yale.edu.

Tony Fu tony.fu@yale.edu.

Christina Huang christina.huang@yale.edu.

Sreejan Kumar sreejan.kumar@yale.edu.

Thomas Liao thomas.liao@yale.edu.

Anand Nanduri anand.nanduri@yale.edu.

Sanya Nijhawan sanya.nijhawan@yale.edu.

Anshuman Radhakrishnan anshuman.radhakrishnan@yale.edu.

Bonnie Rhee bonnie.rhee@yale.edu.

David Schwartz david.schwartz@yale.edu.

Justin Shi justin.shi@yale.edu.

Scott Smith scott.smith@yale.edu.

Isabella Teng isabella.teng@yale.edu.

Kevin Truong kevin.truong@yale.edu.

Aadit Vyas aadit.vyas@yale.edu.

Joanna Wu joanna.j.wu@yale.edu.

Holly Zhou holly.zhou@yale.edu.

Ian Zhou ian.zhou@yale.edu.

1.3.8 Use of outside help

Students are free to discuss homework problems and course material with each other, and to consult with the instructor or a TA. Solutions handed in, however, should be the student’s own work. If a student benefits substantially from hints or solutions received from fellow students or from outside sources, then the student should hand in their solution but acknowledge the outside sources, and we will apportion credit accordingly. Using outside resources in solving a problem is acceptable but plagiarism is not.

1.3.9 Clarifications for homework assignments

From time to time, ambiguities and errors may creep into homework assignments. Questions about the interpretation of homework assignments should be sent to the instructor at james.aspnes@gmail.com. Clarifications will appear in the on-line version of the assignment.

1.3.10 Late assignments

Assignments submitted after the deadline without a Dean’s Excuse are automatically assessed a 2%/hour penalty.

1.4 Introduction

There are two purposes to this course: to teach you to program in the C programming language, and to teach you how to choose, implement, and use data structures and standard programming techniques.

1.4.1 Why should you learn to program in C?

It is the de facto substandard of programming languages. C runs on everything. C lets you write programs that use very few resources. C gives you near-total control over the system, down to the level of pushing around individual bits with your bare hands. C imposes very few constraints on programming style: unlike higher-level languages, C doesn’t have much of an ideology. There are very few programs you can’t write in C. Many of the programming languages people actually use (Visual Basic, perl, python, ruby, PHP, etc.) are executed by interpreters written in C (or C++, an extension to C).

You will learn discipline. C makes it easy to shoot yourself in the foot. You can learn to avoid this by being careful about where you point it. Pain is a powerful teacher of caution.

You will fail CPSC 323 if you don’t learn C really well in CPSC 223 (CS majors only).

On the other hand, there are many reasons why you might not want to use C later in life. It’s missing a lot of features of modern program languages, including:

A garbage collector.

Minimal programmer-protection features like array bounds-checking or a strong type system.

Non-trivial built-in data structures.

Language support for exceptions, namespaces, object-oriented programming, etc.

For most problems where minimizing programmer time and maximizing robustness are more important than minimizing runtime, other languages are a better choice. But for this class, we’ll be using C.

If you want to read a lot of flaming about what C is or is not good for, see http://c2.com/cgi/wiki?CeeLanguage.

1.4.2 Why should you learn about data structures and programming techniques?

For small programs, you don’t need much in the way of data structures. But as soon as you are representing reasonably complicated data, you need some place to store it. Thinking about how you want to store and organize this data can be a good framework for organizing the rest of your program.

Many programming environments will give you a rich collection of built-in data structures as part of their standard library. C does not: unless you use third-party libraries, any data structure you want in C you will have to build yourself. For most data structures this will require an understanding of pointers and storage allocation, mechanisms often hidden in other languages. Understanding these concepts will give you a deeper understanding of how computers actually work, and will both let you function in minimalist environments where you don’t have a lot of support and let you understand what more convenient environments are doing under their abstraction barriers.

The same applies to the various programming techniques we will discuss in this class. While some of the issues that come up are specific to C and similar low-level languages (particular issues involving disciplined management of storage), some techniques will apply no matter what kinds of programs you are writing and all will help in understanding what your computer systems are doing even if some of the details are hidden.

2 The Zoo

The main undergraduate computing facility for Computer Science is the Zoo, located on the third floor of AKW. The Zoo contains a large number of Linux workstations.

You don’t need to do your work for this class in the Zoo, but that is where your assignments will be submitted and tested, so if you do development elsewhere, you will need to copy your files over and make sure that they work there as well.

The best place for information about the Zoo is at http://zoo.cs.yale.edu/. Below are some points that are of particular relevance for CS223 students.

2.1 Getting an account

To get an account in the Zoo, follow the instructions at http://zoo.cs.yale.edu/accounts.html. You will need your NetID and password to sign up for an account.

Even if you already have an account, you still need to use this form to register as a CS 223 student, or you will not be able to submit assignments.

2.2 Getting into the room

The Zoo is located on the third floor of Arthur K Watson Hall, toward the front of the building. If you are a Yale student, your ID should get you into the building and the room. If you are not a student, you will need to get your ID validated in AKW 008a to get in after hours.

2.3 Remote use

There are several options for remote use of the Zoo. The simplest is to use ssh as described in the following section. This will give you a terminal session, which is enough to run anything you need to if you are not trying to do anything fancy. The related program scp can be used to upload and download files.

2.3.1 Terminal access

The best part of Unix is that nothing ever changes. The instructions below still work, and will get you a terminal window in the Zoo:

Date: Mon, 13 Dec 2004 14:34:19 -0500 (EST) From: Jim Faulkner <james.faulkner@yale.edu> Subject: Accessing the Zoo Hello all, I've been asked to write up a quick guide on how to access the Linux computers in the Zoo. For those who need this information, please read on. There are 2 ways of accessing the Zoo nodes, by walking up to one and logging in on the console (the computers are located on the 3rd floor of AKW), or by connecting remotely via SSH. Telnet access is not allowed. SSH clients for various operating systems are available here: http://www.yale.edu/software/ Mac OSX comes with an SSH client by default. A good choice for an SSH client if you run Microsoft Windows is PuTTY: http://www.chiark.greenend.org.uk/~sgtatham/putty/ With the exception of a few legacy accounts, the Zoo uses your campus-wide NetID and password for login access. However, you must sign up for a Zoo account before access is allowed. To sign up for a Zoo account, go to this web page: http://zoo.cs.yale.edu/accounts.html Then login with your campus-wide NetID and password. You may choose a different shell, or set up your account to be enrolled in a class if that is appropriate for you, but neither is necessary. Just click "Submit". Within an hour, your Zoo account will be created, and you will receive more information via e-mail about how to access the Zoo. Users cannot log into zoo.cs.yale.edu (the central file server) directly, they must log into one of the Zoo nodes. Following is the list of Zoo nodes: aphid.zoo.cs.yale.edu lion.zoo.cs.yale.edu bumblebee.zoo.cs.yale.edu macaw.zoo.cs.yale.edu cardinal.zoo.cs.yale.edu monkey.zoo.cs.yale.edu chameleon.zoo.cs.yale.edu newt.zoo.cs.yale.edu cicada.zoo.cs.yale.edu peacock.zoo.cs.yale.edu cobra.zoo.cs.yale.edu perch.zoo.cs.yale.edu cricket.zoo.cs.yale.edu python.zoo.cs.yale.edu frog.zoo.cs.yale.edu rattlesnake.zoo.cs.yale.edu gator.zoo.cs.yale.edu rhino.zoo.cs.yale.edu giraffe.zoo.cs.yale.edu scorpion.zoo.cs.yale.edu grizzly.zoo.cs.yale.edu swan.zoo.cs.yale.edu hare.zoo.cs.yale.edu termite.zoo.cs.yale.edu hippo.zoo.cs.yale.edu tick.zoo.cs.yale.edu hornet.zoo.cs.yale.edu tiger.zoo.cs.yale.edu jaguar.zoo.cs.yale.edu tucan.zoo.cs.yale.edu koala.zoo.cs.yale.edu turtle.zoo.cs.yale.edu ladybug.zoo.cs.yale.edu viper.zoo.cs.yale.edu leopard.zoo.cs.yale.edu zebra.zoo.cs.yale.edu If you have already created an account, you can SSH directly to one of the above computers and log in with your campus-wide NetID and password. You can also SSH to node.zoo.cs.yale.edu, which will connect you to a random Zoo node. Feel free to contact me if you have any questions about the Zoo. thanks, Jim Faulkner Zoo Systems Administrator

2.3.2 GUI access

If for some reason you really want to replicate the full Zoo experience on your own remote machine, you can try running an X server and forwarding your connection.

The instructions below were written by Debayan Gupta in 2013, and may or may not still work.

For Mac or Linux users, typing “ssh -X netID@node.zoo.cs.yale.edu” into a terminal and then running “nautilus” will produce an X window interface.

When on Windows, I usually use XMing (I’ve included a step-by-step guide at the end of this mail).

For transferring files, I use CoreFTP (http://www.coreftp.com). FileZilla (https://filezilla-project.org/) is another option.

Step-by-step guide to XMIng:

You can download Xming from here: http://sourceforge.net/projects/xming/

Download and install. Do NOT launch Xming at the end of your installation.

Once you’ve installed Xming, go to your start menu and find XLaunch (it should be in the same folder as Xming).

Start XLaunch, and select “Multiple Windows”. Leave “Display Number” as its default value. Click next. Select “Start a program”. Click next. Type “nautilus” (or “terminal”, if you want a terminal) into the “Start Program” text area. Select “Using PuTTY (plink.exe)”. Type in the name of the computer (use “node.zoo.cs.yale.edu”) in the “Connect to computer” text box. Type in your netID in the “Login as user” text box (you can leave the password blank). Click next. Make sure “Clipboard” is ticked. Leave everything else blank. Click next. Click “Save Configuration”. When saving, make sure your filename ends with “.xlaunch” - this will let you connect with a click (you won’t need to do all this every time you connect). Click Finish. You will be prompted for your password - enter it. Ignore any security warnings. You now have a remote connection to the Zoo.

For more options and information, you can go to: http://www.straightrunning.com/XmingNotes/

2.3.3 GUI access using FastX

Another possibility may be FastX, a commercial X server that does some extra compression.

The FastX client can be downloaded from https://software.yale.edu/software/fastx-2-client. You may need to supply your NetID and password to access this page. In the past using this software required going through a complicated procedure to get a Yale license key, but this appears to no longer be the case.

After downloading and installing FastX, you should supply node.cs.yale.edu as the machine to connect to unless you have a particular fondness for a specific Zoo node. As with ssh , your login will be your NetID and password.

2.4 Developing on your own machine

Because C is highly portable, there is a good chance you can develop assignment solutions on your own machine and just upload to the Zoo for final testing and submission. Because there are many different kinds of machines out there, I can only offer very general advice about how to do this.

You will need a text editor. I like Vim, which will run on pretty much anything, but you should use whatever you are comfortable with.

You will also need a C compiler that can handle C99. Ideally you will have an environment that looks enough like Linux that you can also run other command-line tools like gdb , make , and possibly git . How you get this depends on your underlying OS.

2.4.1 Linux

Pretty much any Linux distribution will give you this out of the box. You may need to run your package manager to install missing utilities like the gcc C compiler.

2.4.2 OSX

OSX is not Linux, but it is Unix under the hood. You will need a terminal emulator (the built-in Terminal program works, but I like iTerm2. You will also need to set up XCode to get command-line developer tools. The method for doing this seems to vary depending on which version of XCode you have.

You may end up with c99 pointing at clang instead of gcc . Most likely the only difference you will see is the details of the error messages. Remember to test with gcc on the Zoo.

Other packages can be installed using Homebrew. If you are a Mac person you probably already know more about this than I do.

2.4.3 Windows

What you can do here depends on your version of Windows.

For Windows 10, you can install Windows Subsystem for Linux. This gives you the ability to type bash in a console window and get a full-blown Linux installation. You will need to choose a Linux distribution: if you don’t have a preference, I recommend Ubuntu. You will need to use the apt program to install things like gcc . If you use Ubuntu, it will suggest which packages to install if you type a command it doesn’t recognize. You can also run ubuntu.exe from Windows to get a nicer terminal emulator than the default console window.

in a console window and get a full-blown Linux installation. You will need to choose a Linux distribution: if you don’t have a preference, I recommend Ubuntu. For other versions of Windows, you can install a virtualization program like VirtualBox or VMware and run Linux inside it.

You may also be able to develop natively in Windows using Cygwin, but this is probably harder than the other options and may produce surprising portability issues when moving your code to Linux.

2.5 How to compile and run programs

See the chapter on how to use the Zoo for details of particular commands. The basic steps are

Creating the program with a text editor of your choosing. (I like vim for long programs and cat for very short ones.)

for long programs and for very short ones.) Compiling it with gcc .

. Running it.

If any of these steps fail, the next step is debugging. We’ll talk about debugging elsewhere.

2.5.1 Creating the program

Use your favorite text editor. The program file should have a name of the form foo.c ; the .c at the end tells the C compiler the contents are C source code. Here is a typical C program:

2.5.2 Compiling and running a program

Here’s what happens when I compile and run it on the Zoo:

$ c99 -g3 -o count count.c $ ./count Now I will count from 1 to 10 1 2 3 4 5 6 7 8 9 10 $

The first line is the command to compile the program. The dollar sign is my prompt, which is printed by the system to tell me it is waiting for a command. The command calls gcc as c99 with arguments -g3 (enable maximum debugging info), -o (specify executable file name, otherwise defaults to a.out ), count (the actual executable file name), and count.c (the source file to compile). This tells gcc that we should compile count.c to count in C99 mode with maximum debugging info included in the executable file.

The second line runs the output file count . Calling it ./count is necessary because by default the shell (the program that interprets what you type) only looks for programs in certain standard system directories. To make it run a program in the current directory, we have to include the directory name.

2.5.3 Some notes on what the program does

Noteworthy features of this program include:

The #include <stdio.h> in line 1. This is standard C boilerplate, and will appear in any program you see that does input or output. The meaning is to tell the compiler to include the text of the file /usr/include/stdio.h in your program as if you had typed it there yourself. This particular file contains declarations for the standard I/O library functions like puts (put string) and printf (print formatted), as used in the program. If you don’t put it in, your program may or may not still compile. Do it anyway.

Line 3 is a comment; its beginning and end is marked by the /* and */ characters. Comments are ignored by the compiler but can be helpful for other programmers looking at your code (including yourself, after you’ve forgotten why you wrote something).

Lines 5 and 6 declare the main function. Every C program has to have a main function declared in exactly this way—it’s what the operating system calls when you execute the program. The int on Line 3 says that main returns a value of type int (we’ll describe this in more detail later in the chapter on functions), and that it takes two arguments: argc of type int , the number of arguments passed to the program from the command line, and argv , of a pointer type that we will get to eventually, which is an array of the arguments (essentially all the words on the command line, including the program name). Note that it would also work to do this as one line (as K&R typically does); the C compiler doesn’t care about whitespace, so you can format things however you like, subject to the constraint that consistency will make it easier for people to read your code.

Everything inside the curly braces is the body of the main function. This includes The declaration int i; , which says that i will be a variable that holds an int (see the chapter on Integer Types). Line 10, which prints an informative message using puts (discussed in the chapter on input and output. The for loop on Lines 11–13, which executes its body for each value of i from 1 to 10. We’ll explain how for loops work later. Note that the body of the loop is enclosed in curly braces just like the body of the main function. The only statement in the body is the call to printf on Line 12; this includes a format string that specifies that we want a decimal-formatted integer followed by a newline (the

). The return 0; on Line 15 tells the operating system that the program worked (the convention in Unix is that 0 means success). If the program didn’t work for some reason, we could have returned something else to signal an error.



3 The Linux programming environment

The Zoo runs a Unix-like operating system called Linux. Most people run Unix with a command-line interface provided by a shell. Each line typed to the shell tells it what program to run (the first word in the line) and what arguments to give it (remaining words). The interpretation of the arguments is up to the program.

3.1 The shell

When you sign up for an account in the Zoo, you are offered a choice of possible shell programs. The examples below assume you have chosen bash , the Bourne-again shell written by the GNU project. Other shells behave similarly for basic commands.

3.1.1 Getting a shell prompt in the Zoo

When you log in to a Zoo node directly, you may not automatically get a shell window. If you use the default login environment (which puts you into the KDE window manager), you need to click on the picture of the display with a shell in from of it in the toolbar at the bottom of the screen. If you run Gnome instead (you can change your startup environment using the popup menu in the login box), you can click on the foot in the middle of the toolbar. Either approach will pop up a terminal emulator from which you can run emacs, gcc, and so forth.

The default login shell in the Zoo is bash , and all examples of shell command lines given in these notes will assume bash . You can choose a different login shell on the account sign-up page if you want to, but you are probably best off just learning to like bash .

3.1.2 The Unix filesystem

Most of what one does with Unix programs is manipulate the filesystem. Unix files are unstructured blobs of data whose names are given by paths consisting of a sequence of directory names separated by slashes: for example /home/accts/some-user/cs223/hw1.c . At any time you are in a current working directory (type pwd to find out what it is and cd new-directory to change it). You can specify a file below the current working directory by giving just the last part of the pathname. The special directory names . and .. can also be used to refer to the current directory and its parent. So /home/accts/some-user/cs223/hw1.c is just hw1.c or ./hw1.c if your current working directory is /home/accts/some-user/cs223 , cs223/hw1.c if your current working directory is /home/accts/some-user , and ../cs223/hw1.c if your current working directory is /home/accts/some-user/illegal-downloads .

All Zoo machines share a common filesystem, so any files you create or change on one Zoo machine will show up in the same place on all the others.

3.1.3 Unix command-line programs

Here are some handy Unix commands:

man man program will show you the on-line documentation (the man page) for a program (e.g., try man man or man ls ). Handy if you want to know what a program does. On Linux machines like the ones in the Zoo you can also get information using info program , which has an Emacs-like interface. You can also use man function to see documentation for standard library functions. The command man -k string will search for man pages whose titles contain string. Sometimes there is more than one man page with the same name. In this case man -k will distingiush them by different manual section numbers, e.g., printf (1) (a shell command) vs. printf (3) (a library routine). To get a man page from a specific section, use man section name, e.g. man 3 printf . ls ls lists all the files in the current directory. Some useful variants: ls /some/other/dir ; list files in that directory instead.

; list files in that directory instead. ls -l ; long output format showing modification dates and owners. mkdir mkdir dir will create a new directory in the current directory named dir . rmdir rmdir dir deletes a directory. It only works on directories that contain no files. cd cd dir changes the current working directory. With no arguments, cd changes back to your home directory. pwd pwd (“print working directory”) shows what your current directory is. mv mv old-name new-name changes the name of a file. You can also use this to move files between directories. cp cp old-name new-name makes a copy of a file. rm rm file deletes a file. Deleted files cannot be recovered. Use this command carefully. chmod chmod changes the permissions on a file or directory. See the man page for the full details of how this works. Here are some common chmod ’s: chmod 644 file ; owner can read or write the file, others can only read it.

; owner can read or write the file, others can only read it. chmod 600 file ; owner can read or write the file, others can’t do anything with it.

; owner can read or write the file, others can’t do anything with it. chmod 755 file ; owner can read, write, or execute the file, others can read or execute it. This is typically used for programs or for directories (where the execute bit has the special meaning of letting somebody find files in the directory).

; owner can read, write, or execute the file, others can read or execute it. This is typically used for programs or for directories (where the execute bit has the special meaning of letting somebody find files in the directory). chmod 700 file ; owner can read, write, or execute the file, others can’t do anything with it. emacs , gcc , make , gdb , git See corresponding sections.

3.1.4 Stopping and interrupting programs

Sometimes you may have a running program that won’t die. Aside from costing you the use of your terminal window, this may be annoying to other Zoo users, especially if the process won’t die even if you close the terminal window or log out.

There are various control-key combinations you can type at a terminal window to interrupt or stop a running program.

ctrl-C Interrupt the process. Many processes (including any program you write unless you trap SIGINT using the sigaction system call) will die instantly when you do this. Some won’t. ctrl-Z Suspend the process. This will leave a stopped process lying around. Type jobs to list all your stopped processes, fg to restart the last process (or fg %1 to start process %1 etc.), bg to keep running the stopped process in the background, kill %1 to kill process %1 politely, kill -KILL %1 to kill process %1 whether it wants to die or not. ctrl-D Send end-of-file to the process. Useful if you are typing test input to a process that expects to get EOF eventually or writing programs using cat > program.c (not really recommmended). For test input, you are often better putting it into a file and using input redirection ( ./program < test-input-file ); this way you can redo the test after you fix the bugs it reveals. ctrl-\ Quit the process. Sends a SIGQUIT, which asks a process to quit and dump core. Mostly useful if ctrl-C and ctrl-Z don’t work.

If you have a runaway process that you can’t get rid of otherwise, you can use ps g to get a list of all your processes and their process ids. The kill command can then be used on the offending process, e.g. kill -KILL 6666 if your evil process has process id 6666. Sometimes the killall command can simplify this procedure, e.g. killall -KILL evil kills all process with command name evil .

3.1.5 Running your own programs

If you compile your own program, you will need to prefix it with ./ on the command line to tell the shell that you want to run a program in the current directory (called ‘ . ’) instead of one of the standard system directories. So for example, if I’ve just built a program called count , I can run it by typing

$ ./count

Here the “ $ ” is standing in for whatever your prompt looks like; you should not type it.

Any words after the program name (separated by whitespace—spaces and/or tabs) are passed in as arguments to the program. Sometimes you may wish to pass more than one word as a single argument. You can do so by wrapping the argument in single quotes, as in

$ ./count 'this is the first argument' 'this is the second argument'

3.1.6 Redirecting input and output

Some programs take input from standard input (typically the terminal). If you are doing a lot of testing, you will quickly become tired of typing test input at your program. You can tell the shell to redirect standard input from a file by putting the file name after a < symbol, like this:

$ ./count < huge-input-file

A ‘>’ symbol is used to redirect standard output, in case you don’t want to read it as it flies by on your screen:

$ ./count < huge-input-file > huger-output-file

A useful file for both input and output is the special file /dev/null . As input, it looks like an empty file. As output, it eats any characters sent to it:

$ ./sensory-deprivation-experiment < /dev/null > /dev/null

You can also pipe programs together, connecting the output of one to the input of the next. Good programs to put at the end of a pipe are head (eats all but the first ten lines), tail (eats all but the last ten lines), more (lets you page through the output by hitting the space bar, and tee (shows you the output but also saves a copy to a file). A typical command might be something like ./spew | more or ./slow-but-boring | tee boring-output . Pipes can consist of a long train of programs, each of which processes the output of the previous one and supplies the input to the next. A typical case might be:

$ ./do-many-experiments | sort | uniq -c | sort -nr

which, if ./do-many-experiments gives the output of one experiment on each line, produces a list of distinct experimental outputs sorted by decreasing frequency. Pipes like this can often substitute for hours of real programming.

3.2 Text editors

To write your programs, you will need to use a text editor, preferably one that knows enough about C to provide tools like automatic indentation and syntax highlighting. There are three reasonable choices for this in the Zoo: kate , emacs , and vim (which can also be run as vi ). Kate is a GUI-style editor that comes with the KDE window system; it plays nicely with the mouse, but Kate skills will not translate well into other environements. Emacs and Vi have been the two contenders for the One True Editor since the 1970s—if you learn one (or both) you will be able to use the resulting skills everywhere. My personal preference is to use Vi, but Emacs has the advantage of using the same editing commands as the shell and gdb command-line interfaces.

3.2.1 Writing C programs with Emacs

To start Emacs, type emacs at the command line. If you are actually sitting at a Zoo node it should put up a new window. If not, Emacs will take over the current window. If you have never used Emacs before, you should immediately type C-h t (this means hold down the Control key, type h , then type t without holding down the Control key). This will pop you into the Emacs built-in tutorial.

3.2.1.1 My favorite Emacs commands

General note: C-x means hold down Control and press x ; M-x means hold down Alt (Emacs calls it “Meta”) and press x . For M-x you can also hit Esc and then x .

C-h Get help. Everything you could possibly want to know about Emacs is available through this command. Some common versions: C-h t puts up the tutorial, C-h b lists every command available in the current mode, C-h k tells you what a particular sequence of keystrokes does, and C-h l tells you what the last 50 or so characters you typed were (handy if Emacs just garbled your file and you want to know what command to avoid in the future). C-x u Undo. Undoes the last change you made to the current buffer. Type it again to undo more things. A lifesaver. Note that it can only undo back to the time you first loaded the file into Emacs—if you want to be able to back out of bigger changes, use git (described below). C-x C-s Save. Saves changes to the current buffer out to its file on disk. C-x C-f Edit a different file. C-x C-c Quit out of Emacs. This will ask you if you want to save any buffers that have been modified. You probably want to answer yes ( y ) for each one, but you can answer no ( n ) if you changed some file inside Emacs but want to throw the changes away. C-f Go forward one character. C-b Go back one character. C-n Go to the next line. C-p Go to the previous line. C-a Go to the beginning of the line. C-k Kill the rest of the line starting with the current position. Useful Emacs idiom: C-a C-k . C-y “Yank.” Get back what you just killed. TAB Re-indent the current line. In C mode this will indent the line according to Emacs’s notion of how C should be indented. M-x compile Compile a program. This will ask you if you want to save out any unsaved buffers and then run a compile command of your choice (see the section on compiling programs below). The exciting thing about M-x compile is that if your program has errors in it, you can type C-x ` to jump to the next error, or at least where gcc thinks the next error is.

3.2.2 Using Vi instead of Emacs

If you don’t find yourself liking Emacs very much, you might want to try Vim instead. Vim is a vastly enhanced reimplementation of the classic vi editor, which I personally find easier to use than Emacs. Type vimtutor to run the tutorial.

One annoying feature of Vim is that it is hard to figure out how to quit. If you don’t mind losing all of your changes, you can always get out by hitting the Escape key a few times and then typing ~~~\\\ :qa!\\\ ~~~

To run Vim, type vim or vim filename from the command line. Or you can use the graphical version gvim , which pops up its own window.

Vim is a modal editor, meaning that at any time you are in one of several modes (normal mode, insert mode, replace mode, operator-pending mode, etc.), and the interpretation of keystrokes depends on which mode you are in. So typing jjjj in normal mode moves the cursor down four lines, while typing jjjj in insert mode inserts the string jjjj at the current position. Most of the time you will be in either normal mode or insert mode. There is also a command mode entered by hitting : that lets you type longer commands, similar to the Unix command-line or M-x in Emacs.

3.2.2.1 My favorite Vim commands

3.2.2.1.1 Normal mode

:h Get help. (Hit Enter at the end of any command that starts with a colon.) Escape Get out of whatever strange mode you are in and go back to normal mode. You will need to use this whenever you are done typing code and want to get back to typing commands. i Enter insert mode. You will need to do this to type anything. The command a also enters insert mode, but puts new text after the current cursor position instead of before it. u Undo. Undoes the last change you made to the current buffer. Type it again to undo more things. If you undid something by mistake, c- R (control R ) will redo the last undo (and can also be repeated). :w Write the current file to disk. Use :w filename to write it to filename . Use :wa to write all files that you have modified. The command ZZ does the same thing without having to hit Enter at the end. :e filename Edit a different file. :q Quit. Vi will refuse to do this if you have unwritten files. See :wa for how to fix this, or use :q! if you want to throw away your changes and quit anyway. The shortcuts :x and :wq do a write of the current file followed by quitting. h, j, k, l Move the cursor left, down, up, or right. You can also use the arrow keys (in both normal mode and insert mode). x Delete the current character. D Delete to end of line. dd Delete all of the current line. This is a special case of a more general d command. If you precede it with a number, you can delete multiple lines: 5dd deletes the next 5 lines. If you replace the second d with a motion command, you delete until wherever you land: d$ deletes to end of line ( D is faster), dj deletes this line and the line after it, d% deletes the next matching group of parentheses/braces/brackets and whatever is between them, dG deletes to end of file—there are many possibilities. All of these save what you deleted into register "" so you can get them back with p . yy Like dd , but only saves the line to register "" and doesn’t delete it. (Think copy). All the variants of dd work with yy : 5yy , y$ , yj , y% , etc. p Pull whatever is in register "" . (Think paste). << and >> Outdent or indent the current line one tab stop. :make Run make in the current directory. You can also give it arguments, e.g., :make myprog , :make test . Use :cn to go to the next error if you get errors. :! Run a command, e.g., :! echo hello world or :! gdb myprogram . Returns to Vim when the command exits (control-C can sometimes be helpful if your command isn’t exiting when it should). This works best if you ran Vim from a shell window; it doesn’t work very well if Vim is running in its own window.

3.2.2.1.2 Insert mode

control-P and control-N These are completion commands that attempt to expand a partial word to something it matches elsewhere in the buffer. So if you are a good person and have named a variable informativeVariableName instead of ivn , you can avoid having to type the entire word by typing inf <control-P> if it’s the only word in your buffer that starts with inf . control-O and control-I Jump to the last cursor position before a big move / back to the place you jumped from. ESC Get out of insert mode!

3.2.2.2 Settings

Unlike Emacs, Vim’s default settings are not very good for editing C programs. You can fix this by creating a file called .vimrc in your home directory with the following commands:

set shiftwidth=4 set autoindent set backup set cindent set hlsearch set incsearch set showmatch set number syntax on filetype plugin on filetype indent on examples/sample.vimrc

(You can download this file by clicking on the link.)

In Vim, you can type e.g. :help backup to find out what each setting does. Note that because .vimrc starts with a . , it won’t be visible to ls unless you use ls -a or ls -A .

3.3.1 The GNU C compiler gcc

A C program will typically consist of one or more files whose names end with .c . To compile foo.c , you can type gcc foo.c . Assuming foo.c contains no errors egregious enough to be detected by the extremely forgiving C compiler, this will produce a file named a.out that you can then execute by typing ./a.out .

If you want to debug your program using gdb or give it a different name, you will need to use a longer command line. Here’s one that compiles foo.c to foo (run it using ./foo ) and includes the information that gdb needs: gcc -g3 -o foo foo.c

If you want to use C99 features, you will need to tell gcc to use C99 instead of its own default dialect of C. You can do this either by adding the argument -std=c99 as in gcc -std=c99 -o foo foo.c or by calling gcc as c99 as in c99 -o foo foo.c .

By default, gcc doesn’t check everything that might be wrong with your program. But if you give it a few extra arguments, it will warn you about many (but not all) potential problems: c99 -g3 -Wall -pedantic -o foo foo.c .

3.3.2 Make

For complicated programs involving multiple source files, you are probably better off using make than calling gcc directly. Make is a “rule-based expert system” that figures out how to compile programs given a little bit of information about their components.

For example, if you have a file called foo.c , try typing make foo and see what happens.

In general you will probably want to write a Makefile , which is named Makefile or makefile and tells make how to compile programs in the same directory. Here’s a typical Makefile:

Given a Makefile, make looks at each dependency line and asks: (a) does the target on the left hand side exist, and (b) is it older than the files it depends on. If so, it looks for a set of commands for rebuilding the target, after first rebuilding any of the files it depends on; the commands it runs will be underneath some dependency line where the target appears on the left-hand side. It has built-in rules for doing common tasks like building .o files (which contain machine code) from .c files (which contain C source code). If you have a fake target like all above, it will try to rebuild everything all depends on because there is no file named all (one hopes).

3.3.2.1 Make gotchas

Make really really cares that the command lines start with a TAB character. TAB looks like eight spaces in Emacs and other editors, but it isn’t the same thing. If you put eight spaces in (or a space and a TAB), Make will get horribly confused and give you an incomprehensible error message about a “missing separator”. This misfeature is so scary that I avoided using make for years because I didn’t understand what was going on. Don’t fall into that trap—make really is good for you, especially if you ever need to recompile a huge program when only a few source files have changed.

If you use GNU Make (on a zoo node), note that beginning with version 3.78, GNU Make prints a message that hints at a possible SPACEs-vs-TAB problem, like this:

$ make Makefile:23:*** missing separator (did you mean TAB instead of 8 spaces?). Stop.

If you need to repair a Makefile that uses spaces, one way of converting leading spaces into TABs is to use the unexpand program:

$ mv Makefile Makefile.old $ unexpand Makefile.old > Makefile

3.4 Debugging tools

The standard debugger on the Zoo is gdb . Also useful is the memory error checker valgrind . Below are some notes on debugging in general and using these programs in particular.

3.4.1 Debugging in general

Basic method of all debugging:

Know what your program is supposed to do. Detect when it doesn’t. Fix it.

A tempting mistake is to skip step 1, and just try randomly tweaking things until the program works. Better is to see what the program is doing internally, so you can see exactly where and when it is going wrong. A second temptation is to attempt to intuit where things are going wrong by staring at the code or the program’s output. Avoid this temptation as well: let the computer tell you what it is really doing inside your program instead of guessing.

3.4.2 Assertions

Every non-trivial C program should include <assert.h> , which gives you the assert macro (see Appendix B6 of K&R). The assert macro tests if a condition is true and halts your program with an error message if it isn’t:

Compiling and running this program produces the following output:

$ gcc -o no no.c $ ./no no: no.c:6: main: Assertion `2+2 == 5' failed.

Line numbers and everything, even if you compile with the optimizer turned on. Much nicer than a mere segmentation fault, and if you run it under the debugger, the debugger will stop exactly on the line where the assert failed so you can poke around and see why.

3.4.3 The GNU debugger gdb

The standard debugger on Linux is called gdb . This lets you run your program under remote control, so that you can stop it and see what is going on inside.

You can also use ddd , which is a graphical front-end for gdb . There is an extensive tutorial available for ddd , so we will concentrate on the command-line interface to gdb here.

Warning: Though gdb is rock-solid when running on an actual Linux kernel, if you are running on a different underlying operating system like Windows (including Windows Subsystem for Linux) or OS X, it may not work as well, either missing errors that it should catch or in some cases not starting at all. In either case you can try debugging on the Zoo machines instead. For OS X, you might also have better results using the standard OS X debugger lldb , which is similar enough to gdb to do everything gdb can do while being different enough that you will need to learn its own set of commands. Most IDEs that support C also include debugging tools.

Getting back to gdb , we’ll look at a contrived example. Suppose you have the following program bogus.c :

Let’s compile and run it and see what happens. Note that we include the flag -g3 to tell the compiler to include debugging information. This allows gdb to translate machine addresses back into identifiers and line numbers in the original program for us.

$ c99 -g3 -o bogus bogus.c $ ./bogus -34394132 $

That doesn’t look like the sum of 1 to 1000. So what went wrong? If we were clever, we might notice that the test in the for loop is using the mysterious -= operator instead of the <= operator that we probably want. But let’s suppose we’re not so clever right now—it’s four in the morning, we’ve been working on bogus.c for twenty-nine straight hours, and there’s a -= up there because in our befuddled condition we know in our bones that it’s the right operator to use. We need somebody else to tell us that we are deluding ourselves, but nobody is around this time of night. So we’ll have to see what we can get the computer to tell us.

The first thing to do is fire up gdb , the debugger. This runs our program in stop-motion, letting us step through it a piece at a time and watch what it is actually doing. In the example below gdb is run from the command line. You can also run it directly from Emacs with M-x gdb , which lets Emacs track and show you where your program is in the source file with a little arrow, or (if you are logged in directly on a Zoo machine) by running ddd , which wraps gdb in a graphical user interface.

$ gdb bogus GNU gdb 4.17.0.4 with Linux/x86 hardware watchpoint and FPU support Copyright 1998 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux"... (gdb) run Starting program: /home/accts/aspnes/tmp/bogus -34394132 Program exited normally.

So far we haven’t learned anything. To see our program in action, we need to slow it down a bit. We’ll stop it as soon as it enters main , and step through it one line at a time while having it print out the values of the variables.

(gdb) break main Breakpoint 1 at 0x8048476: file bogus.c, line 9. (gdb) run Starting program: /home/accts/aspnes/tmp/bogus Breakpoint 1, main (argc=1, argv=0xbffff9ac) at bogus.c:9 9 sum = 0; (gdb) display sum 1: sum = 1 (gdb) n 10 for(i = 0; i -= 1000; i++) 1: sum = 0 (gdb) display i 2: i = 0 (gdb) n 11 sum += i; 2: i = -1000 1: sum = 0 (gdb) n 10 for(i = 0; i -= 1000; i++) 2: i = -1000 1: sum = -1000 (gdb) n 11 sum += i; 2: i = -1999 1: sum = -1000 (gdb) n 10 for(i = 0; i -= 1000; i++) 2: i = -1999 1: sum = -2999 (gdb) quit The program is running. Exit anyway? (y or n) y $

Here we are using break main to tell the program to stop as soon as it enters main , display to tell it to show us the value of the variables i and sum whenever it stops, and n (short for next ) to execute the program one line at a time.

When stepping through a program, gdb displays the line it will execute next as well as any variables you’ve told it to display. This means that any changes you see in the variables are the result of the previous displayed line. Bearing this in mind, we see that i drops from 0 to -1000 the very first time we hit the top of the for loop and drops to -1999 the next time. So something bad is happening in the top of that for loop, and if we squint at it a while we might begin to suspect that i -= 1000 is not the nice simple test we might have hoped it was.

3.4.3.1 My favorite gdb commands

help Get a description of gdb’s commands. run Runs your program. You can give it arguments that get passed in to your program just as if you had typed them to the shell. Also used to restart your program from the beginning if it is already running. quit Leave gdb, killing your program if necessary. break Set a breakpoint, which is a place where gdb will automatically stop your program. Some examples: - break somefunction stops before executing the first line somefunction . - break 117 stops before executing line number 117. list Show part of your source file with line numbers (handy for figuring out where to put breakpoints). Examples: - list somefunc lists all lines of somefunc . - list 117-123 lists lines 117 through 123. next Execute the next line of the program, including completing any procedure calls in that line. step Execute the next step of the program, which is either the next line if it contains no procedure calls, or the entry into the called procedure. finish Continue until you get out of the current procedure (or hit a breakpoint). Useful for getting out of something you stepped into that you didn’t want to step into. cont (Or continue ). Continue until (a) the end of the program, (b) a fatal error like a Segmentation Fault or Bus Error, or (c) a breakpoint. If you give it a numeric argument (e.g., cont 1000 ) it will skip over that many breakpoints before stopping. print Print the value of some expression, e.g. print i . display Like print , but runs automatically every time the program stops. Useful for watching values that change often. backtrace Show all the function calls on the stack, with arguments. Can be abbreviated as bt . Do bt full if you also want to see local variables in each function. set disable-randomization off Not something you will need every day, but you should try this before running your program if it is producing segmentation faults outside of gdb but not inside. Normally the Linux kernel randomizes the position of bits of your program before running it, to make its response to buffer overflow attacks less predictable. By default, gdb turns this off so that the behavior of your program is consistent from one execution to the next. But sometimes this means that a pointer that had been bad with address randomization (causing a segmentation fault) turns out not to be bad without. This option will restore the standard behavior outside gdb and give you some hope of finding what went wrong.

3.4.3.2 Debugging strategies

In general, the idea behind debugging is that a bad program starts out sane, but after executing for a while it goes bananas. If you can find the exact moment in its execution where it first starts acting up, you can see exactly what piece of code is causing the problem and have a reasonably good chance of being able to fix it. So a typical debugging strategy is to put in a breakpoint (using break ) somewhere before the insanity hits, “instrument” the program (using display ) so that you can watch it going insane, and step through it (using next , step , or breakpoints and cont ) until you find the point of failure. Sometimes this process requires restarting the program (using run ) if you skip over this point without noticing it immediately.

For large or long-running programs, it often makes sense to do binary search to find the point of failure. Put in a breakpoint somewhere (say, on a function that is called many times or at the top of a major loop) and see what the state of the program is after going through the breakpoint 1000 times (using something like cont 1000 ). If it hasn’t gone bonkers yet, try restarting and going through 2000 times. Eventually you bracket the error as occurring (for example) somewhere between the 4000th and 8000th occurrence of the breakpoint. Now try stepping through 6000 times; if the program is looking good, you know the error occurs somewhere between the 6000th and 8000th breakpoint. A dozen or so more experiments should be enough isolate the bug to a specific line of code.

The key to all debugging is knowing what your code is supposed to do. If you don’t know this, you can’t tell the lunatic who thinks he’s Napoleon from lunatic who really is Napoleon. If you’re confused about what your code is supposed to be doing, you need to figure out what exactly you want it to do. If you can figure that out, often it will be obvious what is going wrong. If it isn’t obvious, you can always go back to gdb .

3.4.3.3 Common applications of gdb

Here are some typical classes of bugs and how to squish them with gdb . (The same instructions usually work for ddd .)

3.4.3.3.1 Watching your program run

Compile your program with the -g3 flag. You can still run gdb if you don’t do this, but it won’t be able to show you variable names or source lines. Run gdb with gdb programname. Type break main to stop at the start of the main routine. Run your program with run arguments. The run command stands in for the program name. You can also redirect input as in the shell with run arguments < filename. When the program stops, you can display variables in the current function or expressions involving these variables using display , as in display x , display a[i] , display z+17 . In ddd , double-clicking on a variable name will have the same effect. Use undisplay to get rid of any displays you don’t want. To step through your program, use next (always goes to next line in the current function, not dropping down into function calls), step (go to the next executed line, even if it is inside a called function), finish (run until the current function returns), and cont (run until the end of the program or the next breakpoint).

This can be handy if you don’t particularly know what is going on in your program and want to see.

3.4.3.3.2 Dealing with failed assertions

Run the program as described above. When you hit the bad assert , you will stop several functions deep from where it actually happened. Use up to get up to the function that has the call to assert then use print or display to figure out what is going on.

Example program:

With gdb in action:

$ gcc -g3 -o assertFailed assertFailed.c 22:59:39 (Sun Feb 15) zeniba aspnes ~/g/classes/223/notes/examples/debugging $ gdb assertFailed GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 Copyright (C) 2014 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from assertFailed...done. (gdb) run Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/assertFailed assertFailed: assertFailed.c:12: main: Assertion `x+x == 4' failed. Program received signal SIGABRT, Aborted. 0xb7fdd416 in __kernel_vsyscall () (gdb) up #1 0xb7e43577 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:56 56 ../nptl/sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) up #2 0xb7e469a3 in __GI_abort () at abort.c:89 89 abort.c: No such file or directory. (gdb) up #3 0xb7e3c6c7 in __assert_fail_base (fmt=0xb7f7a8b4 "%s%s%s:%u: %s%sAssertion `%s' failed.

%n", assertion=assertion@entry=0x804850f "x+x == 4", file=file@entry=0x8048500 "assertFailed.c", line=line@entry=12, function=function@entry=0x8048518 <__PRETTY_FUNCTION__.2355> "main") at assert.c:92 92 assert.c: No such file or directory. (gdb) up #4 0xb7e3c777 in __GI___assert_fail (assertion=0x804850f "x+x == 4", file=0x8048500 "assertFailed.c", line=12, function=0x8048518 <__PRETTY_FUNCTION__.2355> "main") at assert.c:101 101 in assert.c (gdb) up #5 0x0804845d in main (argc=1, argv=0xbffff434) at assertFailed.c:12 12 assert(x+x == 4); (gdb) print x $1 = 3

Here we see that x has value 3, which may or may not be the right value, but certainly violates the assertion.

3.4.3.3.3 Dealing with segmentation faults

Very much like the previous case. Run gdb until the segmentation fault hits, then look around for something wrong.

$ gcc -g3 -o segmentationFault segmentationFault.c 23:04:18 (Sun Feb 15) zeniba aspnes ~/g/classes/223/notes/examples/debugging $ gdb segmentationFault GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 [...] Reading symbols from segmentationFault...done. (gdb) run Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/segmentationFault Program received signal SIGSEGV, Segmentation fault. 0x08048435 in main (argc=1, argv=0xbffff434) at segmentationFault.c:13 13 printf("%d

", a[i]); (gdb) print a[i] $1 = 0 (gdb) print i $2 = -1771724

Curiously, gdb has no problem coming up with a value for a[i] . But i looks pretty suspicious.

3.4.3.3.4 Dealing with infinite loops

Run gdb , wait a while, then hit control-C. This will stop gdb wherever it is. If you have an infinite loop, it’s likely that you will be in it, and that the index variables will be doing something surprising. Use display to keep an eye on them and do next a few times.

$ gcc -g3 -o infiniteLoop infiniteLoop.c 23:08:05 (Sun Feb 15) zeniba aspnes ~/g/classes/223/notes/examples/debugging $ gdb infiniteLoop GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 [...] Reading symbols from infiniteLoop...done. (gdb) run Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/infiniteLoop ^C Program received signal SIGINT, Interrupt. main (argc=1, argv=0xbffff434) at infiniteLoop.c:11 11 i *= 37; (gdb) display i 1: i = 0 (gdb) n 10 for(i = 0; i < 10; i += 0) { 1: i = 0 (gdb) n 11 i *= 37; 1: i = 0 (gdb) n 10 for(i = 0; i < 10; i += 0) { 1: i = 0 (gdb) n 11 i *= 37; 1: i = 0 (gdb) n 10 for(i = 0; i < 10; i += 0) { 1: i = 0 (gdb) n 11 i *= 37; 1: i = 0

3.4.3.3.5 Mysterious variable changes

Sometimes pointer botches don’t manifest as good, honest segmentation faults but instead as mysterious changes to seemingly unrelated variables. You can catch these in the act using conditional breakpoints. The downside is that you can only put conditional breakpoints on particular lines.

Here’s a program that violates array bounds (which C doesn’t detect):

In the debugging session below, it takes a couple of attempts to catch the change in x before hitting the failed assertion.

$ gcc -g3 -o mysteryChange mysteryChange.c 23:15:41 (Sun Feb 15) zeniba aspnes ~/g/classes/223/notes/examples/debugging $ gdb mysteryChange GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 [...] Reading symbols from mysteryChange...done. (gdb) run Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange mysteryChange: mysteryChange.c:18: main: Assertion `x == 5' failed. Program received signal SIGABRT, Aborted. 0xb7fdd416 in __kernel_vsyscall () (gdb) list main 2 #include <stdlib.h> 3 #include <assert.h> 4 5 int 6 main(int argc, char **argv) 7 { 8 int x; 9 int a[10]; 10 int i; 11 (gdb) list 12 x = 5; 13 14 for(i = -1; i < 11; i++) { 15 a[i] = 37; 16 } 17 18 assert(x == 5); 19 20 return 0; 21 } (gdb) break 14 if x != 5 Breakpoint 1 at 0x804842e: file mysteryChange.c, line 14. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange mysteryChange: mysteryChange.c:18: main: Assertion `x == 5' failed. Program received signal SIGABRT, Aborted. 0xb7fdd416 in __kernel_vsyscall () (gdb) break 15 if x != 5 Breakpoint 2 at 0x8048438: file mysteryChange.c, line 15. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/aspnes/g/classes/223/notes/examples/debugging/mysteryChange Breakpoint 2, main (argc=1, argv=0xbffff434) at mysteryChange.c:15 15 a[i] = 37; (gdb) print i $1 = 0 (gdb) print a[0] $2 = 134520832 (gdb) print a[-1] $3 = 37 (gdb) print x $4 = 37

One thing to note is that a breakpoint stops before the line it is on executes. So when we hit the breakpoint on line 15 ( gdb having observed that x != 5 is true), i has the value 0, but the damage happened in the previous interation when i was -1. If we want to see exactly what happened then, we’d need to go back in time. We can’t do this, but we could set an earlier breakpoint and run the program again.

3.4.4 Valgrind

The valgrind program can be used to detect some (but not all) common errors in C programs that use pointers and dynamic storage allocation. On the Zoo, you can run valgrind on your program by putting valgrind at the start of the command line:

valgrind ./my-program arg1 arg2 < test-input

This will run your program and produce a report of any allocations and de-allocations it did. It will also warn you about common errors like using unitialized memory, dereferencing pointers to strange places, writing off the end of blocks allocated using malloc , or failing to free blocks.

You can suppress all of the output except errors using the -q option, like this:

valgrind -q ./my-program arg1 arg2 < test-input

You can also turn on more tests, e.g.

valgrind -q --tool=memcheck --leak-check=yes ./my-program arg1 arg2 < test-input

See valgrind --help for more information about the (many) options, or look at the documentation at http://valgrind.org/ for detailed information about what the output means. For some common valgrind messages, see the examples section below.

If you want to run valgrind on your own machine, you may be able to find a version that works at http://valgrind.org. Unfortunately, this is only likely to work if you are running a Unix-like operating system. This does include Linux (either on its own or inside Windows Subsystem for Linux) and OSX, but it does not include stock Windows.

3.4.4.1 Compilation flags

You can run valgrind on any program (try valgrind ls ); it does not require special compilation. However, the output of valgrind will be more informative if you compile your program with debugging information turned on using the -g or -g3 flags (this is also useful if you plan to watch your program running using gdb , ).

3.4.4.2 Automated testing

Unless otherwise specified, automated testing of your program will be done using the script in /c/cs223/bin/vg ; this runs /c/cs223/bin/valgrind with the --tool=memcheck , --leak-check=yes , and -q options, throws away your program’s output, and replaces it with valgrind ’s output. If you have a program named ./prog , running /c/cs223/bin/vg ./prog should produce no output.

3.4.4.3 Examples of some common valgrind errors

Here are some examples of valgrind output. In each case the example program is compiled with -g3 so that valgrind can report line numbers from the source code.

You may also find it helpful to play with this demo program written by the Spring 2018 course staff.

3.4.4.3.1 Uninitialized values

Consider this unfortunate program, which attempts to compare two strings, one of which we forgot to ensure was null-terminated:

Run without valgrind, we see no errors, because we got lucky and it turned out our hand-built string was null-terminated anyway:

$ ./uninitialized a is "a"

But valgrind is not fooled:

$ valgrind -q ./uninitialized ==4745== Conditional jump or move depends on uninitialised value(s) ==4745== at 0x4026663: strcmp (mc_replace_strmem.c:426) ==4745== by 0x8048435: main (uninitialized.c:10) ==4745== ==4745== Conditional jump or move depends on uninitialised value(s) ==4745== at 0x402666C: strcmp (mc_replace_strmem.c:426) ==4745== by 0x8048435: main (uninitialized.c:10) ==4745== ==4745== Conditional jump or move depends on uninitialised value(s) ==4745== at 0x8048438: main (uninitialized.c:10) ==4745==

Here we get a lot of errors, but they are all complaining about the same call to strcmp . Since it’s unlikely that strcmp itself is buggy, we have to assume that we passed some uninitialized location into it that it is looking at. The fix is to add an assignment a[1] = '\0' so that no such location exists.

3.4.4.3.2 Bytes definitely lost

Here is a program that calls malloc but not free :

With no extra arguments, valgrind will not look for this error. But if we turn on --leak-check=yes , it will complain:

$ valgrind -q --leak-check=yes ./missing_free ==4776== 26 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==4776== at 0x4024F20: malloc (vg_replace_malloc.c:236) ==4776== by 0x80483F8: main (missing_free.c:9) ==4776==

Here the stack trace in the output shows where the bad block was allocated: inside malloc (specifically the paranoid replacement malloc supplied by valgrind ), which was in turn called by main in line 9 of missing_free.c . This lets us go back and look at what block was allocated in that line and try to trace forward to see why it wasn’t freed. Sometimes this is as simple as forgetting to include a free statement anywhere, but in more complicated cases it may be because I somehow lose the pointer to the block by overwriting the last variable that points to it or by embedding it in some larger structure whose components I forget to free individually.

3.4.4.3.3 Invalid write or read operations

These are usually operations that you do off the end of a block from malloc or on a block that has already been freed.

An example of the first case:

==7141== Invalid write of size 1 ==7141== at 0x804843B: main (invalid_operations.c:12) ==7141== Address 0x419a029 is 0 bytes after a block of size 1 alloc'd ==7141== at 0x4024F20: malloc (vg_replace_malloc.c:236) ==7141== by 0x8048428: main (invalid_operations.c:10) ==7141== ==7141== Invalid read of size 1 ==7141== at 0x4026063: __GI_strlen (mc_replace_strmem.c:284) ==7141== by 0x409BCE4: puts (ioputs.c:37) ==7141== by 0x8048449: main (invalid_operations.c:14) ==7141== Address 0x419a029 is 0 bytes after a block of size 1 alloc'd ==7141== at 0x4024F20: malloc (vg_replace_malloc.c:236) ==7141== by 0x8048428: main (invalid_operations.c:10) ==7141==

An example of the second:

==7144== Invalid write of size 1 ==7144== at 0x804846D: main (freed_block.c:13) ==7144== Address 0x419a028 is 0 bytes inside a block of size 2 free'd ==7144== at 0x4024B3A: free (vg_replace_malloc.c:366) ==7144== by 0x8048468: main (freed_block.c:11) ==7144== ==7144== Invalid write of size 1 ==7144== at 0x8048477: main (freed_block.c:14) ==7144== Address 0x419a029 is 1 bytes inside a block of size 2 free'd ==7144== at 0x4024B3A: free (vg_replace_malloc.c:366) ==7144== by 0x8048468: main (freed_block.c:11) ==7144== ==7144== Invalid read of size 1 ==7144== at 0x4026058: __GI_strlen (mc_replace_strmem.c:284) ==7144== by 0x409BCE4: puts (ioputs.c:37) ==7144== by 0x8048485: main (freed_block.c:16) [... more lines of errors deleted ...]

In both cases the problem is that we are operating on memory that is not guaranteed to be allocated to us. For short programs like these, we might get lucky and have the program work anyway. But we still want to avoid bugs like this because we might not get lucky.

How do we know which case is which? If I write off the end of an existing block, I’ll see something like Address 0x419a029 is 0 bytes after a block of size 1 alloc'd , telling me that I am working on an address after a block that is still allocated. When I try to write to a freed block, the message changes to Address 0x419a029 is 1 bytes inside a block of size 2 free'd , where the free'd part tells me I freed something I probably shouldn’t have. Fixing the first class of bugs is usually just a matter of allocating a bigger block (but don’t just do this without figuring out why you need a bigger block, or you’ll just be introducing random mutations into your code that may cause other problems elsewhere). Fixing the second class of bugs usually involves figuring out why you freed this block prematurely. In some cases you may need to re-order what you are doing so that you don’t free a block until you are completely done with it.

3.4.5 Not recommended: debugging output

A tempting but usually bad approach to debugging is to put lots of printf statements in your code to show what is going on. The problem with this compared to using assert is that there is no built-in test to see if the output is actually what you’d expect. The problem compared to gdb is that it’s not flexible: you can’t change your mind about what is getting printed out without editing the code. A third problem is that the output can be misleading: in particular, printf output is usually buffered, which means that if your program dies suddenly there may be output still in the buffer that is never flushed to stdout . This can be very confusing, and can lead you to believe that your program fails earlier than it actually does.

If you really need to use printf or something like it for debugging output, here are a few rules of thumb to follow to mitigate the worst effects:

Use fprintf(stderr, ...) instead of printf(...) ; this allows you to redirect your program’s regular output somewhere that keeps it separate from the debugging output (but beware of misleading interleaving of the two streams—buffering may mean that output to stdout and stderr appears to arrive out of order). It also helps that output to stderr is usually unbuffered, avoiding the problem of lost output. If you must output to stdout , put fflush(stdout) after any output operation you suspect is getting lost in the buffer. The fflush function forces any buffered output to be emitted immediately. Keep all arguments passed to printf as simple as possible and beware of faults in your debugging code itself. If you write printf("a[key] == %d

", a[key]) and key is some bizarre value, you will never see the result of this printf because your program will segfault while evaluating a[key] . Naturally, this is more likely to occur if the argument is a[key]->size[LEFTOVERS].cleanupFunction(a[key]) than if it’s just a[key] , and if it happens it will be harder to figure out where in this complex chain of array indexing and pointer dereferencing the disaster happened. Better is to wait for your program to break in gdb , and use the print statement on increasingly large fragments of the offending expression to see where the bogus array index or surprising null pointer is hiding. Wrap your debugging output in an #ifdef so you can turn it on and off easily.

Bearing in mind that this is a bad idea, here is an example of how one might do it as well as possible:

Note that we get much more useful information if we run this under gdb (which will stop exactly on the bad line in init ), but not seeing the result of the fputs at least tells us something.

3.5 Performance tuning

Chapter 7 of Kernighan and Pike, The Practice of Programming (Addison-Wesley, 1998) gives an excellent overview of performance tuning. This page will be limited to some Linux-specific details and an example.

3.5.1 Timing under Linux

Use time , e.g.

$ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/words real 0m0.010s user 0m0.006s sys 0m0.004s

This measures “real time” (what it sounds like), “user time” (the amount of time the program runs), and “system time” (the amount of time the operating system spends supporting your program, e.g. by loading it from disk and doing I/O). Real time need not be equal to the sum of user time and system time, since the operating system may be simultaneously running other programs.

Particularly for fast programs, times can vary from one execution to the next, e.g.

$ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/words real 0m0.009s user 0m0.008s sys 0m0.001s $ time wc /usr/share/dict/words 45378 45378 408865 /usr/share/dict/words real 0m0.009s user 0m0.007s sys 0m0.002s

This arises because of measurement errors and variation in how long different operations take. But usually the variation will not be much.

Note also that time is often a builtin operation of your shell, so the output format may vary depending on what shell you use.

3.5.2 Profiling with valgrind

The problem with time is that it only tells you how much time your whole program took, but not where it spent its time. This is similar to looking at a program without a debugger: you can’t see what’s happening inside. If you want to see where your program is spending its time, you need to use a profiler.

The specific profiler we will use in this section is callgrind , a tool built into valgrind , which we’ve been using elsewhere to detect pointer disasters and storage leaks. Full documentation for callgrind can be found at http://valgrind.org/docs/manual/cl-manual.html, but we’ll give an example of typical use here.

Here is an example of a program that is unreasonably slow for what it is doing.

This program defines several functions for processing null-terminated strings: replicate , which concatenates many copies of some string together, and copyEvenCharacters , which copies every other character in a string to a given buffer. Unfortunately, both functions contain a hidden inefficiency arising from their use of the standard C library string functions.

The runtime of the program is not terrible, but not as sprightly as we might expect given that we are working on less than half a megabyte of text:

$ time ./slow abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd acacacacacacacacacac abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more] acacacacacacacacacacacacacacacacacacacac[199960 more] real 0m3.171s user 0m3.164s sys 0m0.001s

So we’d like to make it faster.

In this particular case, the programmer was kind enough to identify the problems in the original code in comments, but we can’t always count on that. But we can use the callgrind tool built into valgrind to find out where our program is spending most of its time.

To run callgrind , call valgrind with the --tool=callgrind option, like this:

$ time valgrind --tool=callgrind ./slow ==5714== Callgrind, a call-graph generating cache profiler ==5714== Copyright (C) 2002-2017, and GNU GPL'd, by Josef Weidendorfer et al. ==5714== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info ==5714== Command: ./slow ==5714== ==5714== For interactive control, run 'callgrind_control -h'. abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd acacacacacacacacacac abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more] acacacacacacacacacacacacacacacacacacacac[199960 more] ==5714== ==5714== Events : Ir ==5714== Collected : 15339385208 ==5714== ==5714== I refs: 15,339,385,208 real 1m31.965s user 1m31.515s sys 0m0.037s

I’ve include time at the start of the command line to make it clear just how much of a slowdown you can expect from using valgrind for this purpose. Note that valgrind only prints a bit of summary data while executing. To get a full report, we use a separate program callgrind_annotate :

$ callgrind_annotate --auto=yes --inclusive=yes > slow.callgrind

Here I sent the output to a file slow.callgrind so I could look at it in more detail in my favorite text editor, since the actual report is pretty huge. The --auto=yes argument tells callgrind_annotate to show how many instructions were executed as part of each line of source code, and the --inclusive=yes argument tells use that in its report it should charge instructions executed in some function both to that function and to all functions responsible for calling it. This is usually what you want to figure out where things are going wrong.

The first thing to look at in slow.callgrind is the table showing which functions are doing most of the work:

-------------------------------------------------------------------------------- Ir file:function -------------------------------------------------------------------------------- 15,339,385,208 ???:0x0000000000000dd0 [/usr/lib64/ld-2.25.so] 15,339,274,304 ???:_start [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow] 15,339,274,293 /usr/src/debug/glibc-2.25-123-gedcf13e25c/csu/../csu/libc-start.c:(below main) [/usr/lib64/libc-2.25.so] 15,339,273,103 slow.c:main [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow] 15,339,273,103 /home/accts/aspnes/g/classes/223/notes/examples/profiling/slow.c:main 11,264,058,263 slow.c:copyEvenCharacters [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow] 11,260,141,740 /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/strlen.S:strlen [/usr/lib64/ld-2.25.so] 4,075,049,055 slow.c:replicate [/home/accts/aspnes/g/classes/223/notes/examples/profiling/slow] 4,074,048,083 /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/multiarch/strcat-ssse3.S:__strcat_ssse3 [/usr/lib64/libc-2.25.so] 108,795 /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/rtld.c:_dl_start [/usr/lib64/ld-2.25.so]

Since each function is charge for work done by its children, the top of the list includes various setup functions included automatically by the C compiler, followed by main . Inside main , we see that the majority of the work is done in copyEvenCharacters , with a substantial chunk in replicate . The suspicious similarity in numbers suggests that most of these instructions in copyEvenCharacters are accounted for by calls to strlen and in replicate by calls to __strcat_sse3 , which happens to be an assembly-language implementation of strcat (hence the .S in the source file name) that uses the special SSE instructions in the x86 instruction set to speed up copying.

We can confirm this suspicion by looking at later parts of the file, which annotate the source code with instruction counts.

The annotated version of slow.c includes this annotated version of replicate , showing roughly 4 billion instructions executed in __strcat_sse3 :

. char * . replicate(char *dest, const char *src, int n) 12 { . /* truncate dest */ 4 dest[0] = '\0'; . . /* BAD: each call to strcat requires walking across dest */ 400,050 for(int i = 0; i < n; i++) { 600,064 strcat(dest, src); 836 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/../sysdeps/x86_64/dl-trampoline.h:_dl_runtime_resolve_xsave (1x) 4,074,048,083 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/multiarch/strcat-ssse3.S:__strcat_ssse3 (100009x) . } . 2 return dest; 4 }

Similarly, the annotated version of copyEvenCharacters shows that 11 billion instructions were executed in strlen :

. char * . copyEvenCharacters(char *dest, const char *src) 12 { . int i; . int j; . . /* BAD: Calls strlen on every pass through the loop */ 2,000,226 for(i = 0, j = 0; i < strlen(src); i += 2, j++) { 11,260,056,980 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/string/../sysdeps/x86_64/strlen.S:strlen (200021x) 825 => /usr/src/debug/glibc-2.25-123-gedcf13e25c/elf/../sysdeps/x86_64/dl-trampoline.h:_dl_runtime_resolve_xsave (1x) 2,000,200 dest[j] = src[i]; . } . 10 dest[j] = '\0'; . 2 return dest; 8 }

This gives a very strong hint for fixing the program: cut down on the cost of calling strlen and strcat .

Fixing copyEvenCharacters is trivial. Because the length of src doesn’t change, we can call strlen once and save the value in a variable:

Fixing replicate is trickier. The trouble with using strcat is that every time we call strcat(dest, src) , strcat has to scan down the entire dest string to find the end, which (a) gets more expensive as dest gets longer, and (b) involves passing over the same non-null initial characters over and over again each time we want to add a few more characters. The effect of this is that we turn what should be an O(n)-time process of generating a string of n characters into something that looks more like O(n2). We can fix this by using pointer arithmetic to keep track of the end of dest ourselves, which also allows us to replace strcat with memcpy , which is likely to be faster since it doesn’t have to check for nulls. Here’s the improved version:

The result of applying both of these fixes can be found in fast.c. This runs much faster than slow :

abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd acacacacacacacacacac abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd[399960 more] acacacacacacacacacacacacacacacacacacacac[199960 more] real 0m0.003s user 0m0.001s sys 0m0.001s

3.5.3 Profiling with gprof

If you can’t use valgrind for profiling, don’t like the output you get from it, or are annoyed by the huge slowdown when profiling your program, you may be able to get similar results from an older program gprof , which is closely tied to the gcc compiler. Unlike valgrind , which simulates an x86 CPU one machine-code instruction at a time, gprof works by having gcc add extra code to your program to track function calls and do sampling at runtime to see where your program is spending its time. The cost of this approach is that you get a bit less accuracy. I have also found gprof to be tricky to get working right on some operating systems.

Here’s a short but slow program for calculating the number of primes less than some limit passed as argv[1] :

And now we’ll time countPrimes 100000 :

$ c99 -g3 -o countPrimes countPrimes.c $ time ./countPrimes 100000 9592 real 0m4.711s user 0m4.608s sys 0m0.004s

This shows that the program took just under five seconds of real time, of which most was spent in user mode and a very small fraction was spent in kernel (sys) mode. The user-mode part corresponds to the code we wrote and any library routines we call that don’t require special privileges from the operation system. The kernel-mode part will mostly be I/O (not much in this case). Real time is generally less useful than CPU time, because it depends on how loaded the CPU is. Also, none of these times are especially precise, because the program only gets charged for time on a context switch (when it switches between user and kernel mode or some other program takes over the CPU for a bit) or when the kernel decides to see what it is up to (typically every 10 milliseconds).

The overall cost is not too bad, but the reason I picked 100000 and not some bigger number was that it didn’t terminate fast enough for larger inputs. We’d like to see why it is taking so long, to have some idea what to try to speed up. So we’ll compile it with the -pg option to gcc , which inserts profiling code that counts how many times each function is called and how long (on average) each call takes.

Because the profile is not very smart about shared libraries, we also including the --static option to force the resulting program to be statically linked. This means that all the code that is used by the program is baked into the executable instead of being linked in at run-time. (Normally we don’t do this because it makes for big executables and big running programs, since statically-linked libraries can’t be shared between more than one running program.)

$ c99 -pg --static -g3 -o countPrimes countPrimes.c $ time ./countPrimes 100000 9592 real 0m4.723s user 0m4.668s sys 0m0.000s

Hooray! We’ve made the program slightly slower. But we also just produced a file gmon.out that we can read with gprof . Note that we have to pass the name of the program so that gprof can figure out which executable generated gmon.out .

$ gprof countPrimes Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 100.00 4.66 4.66 100000 0.00 0.00 isPrime 0.00 4.66 0.00 1 0.00 4.66 countPrimes 0.00 4.66 0.00 1 0.00 4.66 main [...much explanatory text deleted]

It looks like we are spending all of our time in isPrime , at least if we read the columns on the left. The per-call columns are not too helpful because of granularity: isPrime is too fast for the profiler to wake up and detect how long it runs for. The total columns are less suspicious because they are obtained by sampling: from time to time, the profiler looks and sees what function it’s in, and charges each function a fraction of the total CPU time proportional to how often it gets sampled. So we probable aren’t really spending zero time in countPrimes and main , but the amount of time we do spend is small enough not to be detected.

This is handy because it means we don’t need to bother trying to speed up the rest of the program. We have two things we can try:

Call isPrime less. Make isPrime faster.

Let’s start by seeing if we can make isPrime faster.

What isPrime is doing is testing if a number n is prime by the most direct way possible: dividing by all numbers less than n until it finds a factor. That’s a lot of divisions: if n is indeed prime, it’s linear in n . Since division is a relatively expensive operation, the first thing to try is to get rid of some.

Here’s a revised version of isPrime :

examples/profiling/countPrimesSkipEvenFactors.c

The trick is to check first if n is divisible by 2 , and only test odd potential factors thereafter. This requires some extra work to handle 2, but maybe the extra code complexity will be worth it.

Let’s see how the timing goes:

$ c99 -pg --static -g3 -o countPrimes ./countPrimesSkipEvenFactors.c $ time ./countPrimes 100000 9592 real 0m2.608s user 0m2.400s sys 0m0.004s $ gprof countPrimes Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 100.00 2.29 2.29 100000 0.00 0.00 isPrime 0.00 2.29 0.00 1 0.00 2.29 countPrimes 0.00 2.29 0.00 1 0.00 2.29 main [...]

Twice as fast! And the answer is still the same, too—this is important.

Can we test even fewer factors? Suppose n has a non-trivial factor x . Then n equals x*y for some y which is also nontrivial. One of x or y will be no bigger than the square root of n . So perhaps we can stop when we reach the square root of n ,

Let’s try it:

examples/profiling/countPrimesSqrt.c

I added +1 to the return value of sqrt both to allow for factor to be equal to the square root of n , and because the output of sqrt is not exact, and it would be embarrassing if I announced that 25 was prime because I stopped at 4.9999999997.

Using the math library not only requires including <math.h> but also requires compiling with the -lm flag after all .c or .o files, to link in the library routines:

$ c99 -pg --static -g3 -o countPrimes ./countPrimesSqrt.c -lm $ time ./countPrimes 1000000 78498 real 0m1.008s user 0m0.976s sys 0m0.000s $ gprof countPrimes Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 50.00 0.02 0.02 100000 0.00 0.00 isPrime 50.00 0.04 0.02 __sqrt_finite 0.00 0.04 0.00 1 0.00 20.00 countPrimes 0.00 0.04 0.00 1 0.00 20.00 main [...]

Whoosh!

Can we optimize further? Let’s see what happens on a bigger input:

$ time ./countPrimes 1000000 78498 real 0m0.987s user 0m0.960s sys 0m0.000s $ gprof countPrimes Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 51.04 0.49 0.49 __sqrt_finite 44.79 0.92 0.43 1000000 0.00 0.00 isPrime 3.65 0.96 0.04 sqrt 0.52 0.96 0.01 1 5.00 435.00 main 0.00 0.96 0.00 1 0.00 430.00 countPrimes [...]

This is still very good, although we’re spending a lot of time in sqrt (more specifically, its internal helper routine __sqrt_finite ). Can we do better?

Maybe moving the sqrt out of the loop in isPrime will make a difference:

examples/profiling/countPrimesSqrtOutsideLoop.c

$ c99 -pg --static -g3 -o countPrimes ./countPrimesSqrtOutsideLoop.c -lm $ time ./countPrimes 1000000 78498 real 0m0.413s user 0m0.392s sys 0m0.000s $ gprof countPrimes Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 97.44 0.38 0.38 1000000 0.00 0.00 isPrime 2.56 0.39 0.01 1 10.00 390.00 countPrimes 0.00 0.39 0.00 1 0.00 390.00 main [...]

This worked! We are now spending almost so little time in sqrt that the profiler doesn’t notice it.

What if we get rid of the call to sqrt and test if factor * factor <= n instead? This way we could dump the math library:

examples/profiling/countPrimesSquaring.c

$ c99 -pg --static -g3 -o countPrimes ./countPrimesSquaring.c $ time ./countPrimes 1000000 78498 real 0m0.450s user 0m0.428s sys 0m0.000s

This is slower, but not much slower. We might need to decide how much we care about avoiding floating-point computation in our program.

At this point we could decide that countPrimes is fast enough, or maybe we could look for further improvements, say, by testing out many small primes at the beginning instead of just 2 , calling isPrime only on odd values of i , or reading a computational number theory textbook to find out how we ought to be doing this. A reasonable strategy for code for your own use is often to start running one version and make improvements on a separate copy while it’s running. If the first version terminates before you are done writing new code, it’s probably fast enough.

3.5.3.1 Effect of optimization during compilation

We didn’t use any optimization flags for this example, because the optimizer can do a lot of rewriting that can make the output of the profiler confusing. For example, at high optimization levels, the compiler will often avoid function-call overhead by inserting the body of a helper function directly into its caller. But this can make a big difference in performance, so in real life you will want to compile with optimization turned on. Here’s how the performance of countPrimes 100000 is affected by optimization level:

Version No optimization With -O1 With -O2 With -O3 countPrimes.c 4.600 4.060 3.928 3.944 countPrimesSkipEvenFactors.c 2.260 1.948 1.964 1.984 countPrimesSqrt.c 0.036 0.028 0.028 0.028 countPrimesSqrtOutsideLoop.c 0.012 0.012 0.008 0.008 countPrimesSquaring.c 0.012 0.012 0.008 0.012

In each case, the reported time is the sum of user and system time in seconds.

For the smarter routines, more optimization doesn’t necessarily help, although some of this may be experimental error since I was too lazy to get a lot of samples by running each program more than once, and the times for the faster programs are so small that granularity is going to be an issue.

Here’s the same table using countPrimes 10000000 on the three fastest programs:

Version No optimization With -O1 With -O2 With -O3 countPrimesSqrt.c 24.236 18.840 18.720 18.564 countPrimesSqrtOutsideLoop.c 9.388 9.364 9.368 9.360 countPrimesSquaring.c 9.748 9.248 9.236 9.160

Again there are the usual caveats that I am a lazy person and should probably be doing more to deal with sampling and granularity issues, but if you believe these numbers, we actually win by going to countPrimesSquaring once the optimizer is turned on. I suspect that it is benefiting from strength reduction, which would generate the product factor*factor in isPrime incrementally using addition rather than multiplying from scratch each time.

It’s also worth noting that the optimizer works better if we leave a lot of easy optimization lying around. For countPrimesSqrt.c , my guess is that most of the initial gains are from avoiding function call overhead on sqrt by compiling it in-line. But even the optimizer is not smart enough to recognize that we are computing the same value over and over again, so we still win by pulling sqrt out of the loop in countPrimesSqrtOutsideLoop.c .

If I wanted to see if my guesses about the optimizer were correct, there I could use gcc -S and look at the assembler code. But see earlier comments about laziness.

3.6 Version control

When you are programming, you will make mistakes. If you program long enough, these will eventually include true acts of boneheadedness like accidentally deleting all of your source files. You are also likely to spend some of your time trying out things that don’t work, at the end of which you’d like to go back to the last version of your program that did work. All these problems can be solved by using a version control system.

There are six respectable version control systems installed on the Zoo: rcs , cvs , svn , bzr , hg , and git . If you are familiar with any of them, you should use that. If you have to pick one from scratch, I recommend using git . A brief summary of git is given below. For more details, see the tutorials available at http://git-scm.com.

3.6.1 Setting up Git

Typically you run git inside a directory that holds some project you are working on (say, hw1 ). Before you can do anything with git , you will need to create the repository, which is a hidden directory .git that records changes to your files:

$ mkdir git-demo $ cd git-demo $ git init Initialized empty Git repository in /home/classes/cs223/class/aspnes.james.ja54/git-demo/.git/

Now let’s create a file and add it to the repository:

$ echo 'int main(int argc, char **argv) { return 0; }' > tiny.c $ git add tiny.c

The git status command will tell us that Git knows about tiny.c , but hasn’t commited the changes to the repository yet:

$ git status # On branch master # # Initial commit # # Changes to be committed: # (use "git rm --cached <file>..." to unstage) # # new file: tiny.c #

The git commit command will commit the actual changes, along with a message saying what you did. For short messages, the easiest way to do this is to include the message on the command line:

$ git commit -a -m"add very short C program" [master (root-commit) 5393616] add very short C program Committer: James Aspnes <ja54@tick.zoo.cs.yale.edu> Your name and email address were configured automatically based on your username and hostname. Please check that they are accurate. You can suppress this message by setting them explicitly: git config --global user.name "Your Name" git config --global user.email you@example.com If the identity used for this commit is wrong, you can fix it with: git commit --amend --author='Your Name <you@example.com>' 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 tiny.c

The -a argument tells Git to include any changes I made to files it already knows about. The -m argument sets the commit message.

Because this is the first time I ever did a commit, and because I didn’t tell Git who I was before, it complains that its guess for my name and email address may be wrong. It also tells me what to do to get it to shut up about this next time:

$ git config --global user.name "James Aspnes" $ git config --global user.email "aspnes@cs.yale.edu" $ git commit --amend --author="James Aspnes <aspnes@cs.yale.edu>" -m"add a very short C program" [master a44e1e1] add a very short C program 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 tiny.c

Note that I repeated the -m business to git commit --amend ; if I hadn’t, it would have run the default editor ( vim ) to let me edit my commit message. If I don’t like vim , I can change the default using git config --global core.editor , e.g.:

$ git config --global core.editor "emacs -nw"

I can see what commits I’ve done so far using git log :

$ git log commit a44e1e195de4ce785cd95cae3b93c817d598a9ee Author: James Aspnes <aspnes@cs.yale.edu> Date: Thu Dec 29 20:21:21 2011 -0500 add a very short C program

3.6.2 Editing files

Suppose I edit tiny.c using my favorite editor to turn it into the classic hello-world program:

I can see what files have changed using git status :

$ git status # On branch master # Changed but not updated: # (use "git add <file>..." to update what will be committed) # (use "git checkout -- <file>..." to discard changes in working directory) # # modified: tiny.c # no changes added to commit (use "git add" and/or "git commit -a")

Notice how Git reminds me to use git commit -a to include these changes in my next commit. I can also do git add tiny.c if I just want include the changes to tiny.c (maybe I made changes to a different file that I want to commit separately), but usually that’s too much work.

If I want to know the details of the changes since my last commit, I can do git diff :

$ git diff diff --git a/tiny.c b/tiny.c index 0314ff1..f8d9dcd 100644 --- a/tiny.c +++ b/tiny.c @@ -1 +1,8 @@ -int main(int argc, char **argv) { return 0; } +#include <stdio.h> + +int +main(int argc, char **argv) +{ + puts("hello, world"); + return 0; +}

Since I like these changes, I do a commit:

$ git commit -a -m"expand previous program to hello world" [master 13a73be] expand previous program to hello world 1 files changed, 8 insertions(+), 1 deletions(-)

Now there are two commits in my log:

$ git log | tee /dev/null commit 13a73bedd3a48c173898d1afec05bd6fa0d7079a Author: James Aspnes <aspnes@cs.yale.edu> Date: Thu Dec 29 20:34:06 2011 -0500 expand previous program to hello world commit a44e1e195de4ce785cd95cae3b93c817d598a9ee Author: James Aspnes <aspnes@cs.yale.edu> Date: Thu Dec 29 20:21:21 2011 -0500 add a very short C program

3.6.3 Renaming files

You can rename a file with git mv . This is just like regular mv , except that it tells Git what you are doing.

$ git mv tiny.c hello.c $ git status # On branch master # Changes to be committed: # (use "git reset HEAD <file>..." to unstage) # # renamed: tiny.c -> hello.c #

These changes don’t get written to the repository unless you do another git commit :

$ git commit -a -m"give better name to hello program" [master 6d2116c] give better name to hello program 1 files changed, 0 insertions(+), 0 deletions(-) rename tiny.c => hello.c (100%)

3.6.4 Adding and removing files

To add a file, create it and then call git add :

$ cp hello.c goodbye.c $ git status # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # # goodbye.c nothing added to commit but untracked files present (use "git add" to track