780.20: 1094 Session 9

Handouts: Printout of Circle class files, private_vs_public.cpp, and square_test.cpp; "Using the GDB Debugger" and GDB summary sheet.

In this session we'll take a brief break from physics and computational methods to look at some programming tools and take a further look at C++ classes.

Your goals for today (as time permits):

Using the GDB Debugger

We'll step through a contrived example that illustrates the basic commands and capabilities of a debugger (in this case, the GNU debugger gdb). We will use the command-line ("no windows") version of gdb. There are graphical interfaces (such as ddd) that are much nicer to use for more extensive debugging, but it will be worthwhile to start with the simple, primitive version.

  1. Cygwin and Mac users should run from fox with: ssh -Y USERNAME@fox
    The -Y allows editors and other programs to pop up windows. If off campus, use fox.mps.ohio-state.edu (or any other public linux machine). Copy session09.zip to the current directory (".") and unzip:
    cp ~ntg/session09.zip .
    unzip session09.zip
    cd session_09

    You can open another xterm on fox with xterm &
  2. When debugging, you may find it convenient to have two terminal windows open: one to re-compile and link a sample code and another to run gdb in. (Actually, you can interact with gdb directly through emacs, but we won't go into that here.)
  3. Go through the example from the handout "Using the GDB Debugger". The code to debug is check_primes.cpp (a copy is also provided, called check_primes_orig.cpp, so that you can go back to the original if necessary). We've also created a makefile that doesn't use any of our usual warning flags (at first!).

Optimization 101: Squaring a Number

One of the most common floating-point operations is to square a number. Two ways to square x are: pow(x,2) and x*x. Let's test how efficient they are.

  1. Look at the printout for the square_test.cpp code. It implements these two ways of squaring a number. The "clock" function from time.h is used to find the elapsed time. Each operation is executed a large number of times (determined by "repeat") so that we get a reasonably accurate timing.
  2. We've set the optimization to its lowest value, -O0 ("minus oh zero"), to start in make_square_test.
  3. Compile square_test.cpp (using make_square_test) and run it. Adjust "repeat" if the minimum time is too small. Record the times. Which way to square x is more efficient?

  4. If you have an expression (rather than just x) to square, coding (expression)*(expression) is awkward and hard to read. Wouldn't it be better to call a function (e.g., squareit(expression)? Add to square_test.cpp a function:
    double squareit (double x)
    that returns x*x. Add a section to the code that times how long this takes (just copy one of the other timing sections and edit it appropriately, making sure to keep the "final y" cout statement). How does it compare to the others? What is the "overhead" in calling a function (that is, how much extra time does it take)? When is the overhead worthwhile?

  5. Another alternative, common from C programming: use #define to define a macro that squares a number. Add
    #define sqr(z) ((z)*(z))
    somewhere before the start of main. (The extra ()'s are safeguards against unexpected behavior; always include them!) Add a section to the code to time how long this macro takes; what do you find?

  6. One final alternative: add an "inline" function called square:
    inline double square (double x) { return (x*x); };
    that is a function prototype and the function itself. Put it up top with the squareit prototype. Add a section to the code to time how long this function takes. What is your conclusion about which of these methods to use?

  7. Finally, we'll try the simplest way to optimize a code: let the compiler do it for you! Change the compile flag -O0 (no optimization) to -O2 (that's the uppercase letter O, not a zero). Recompile and run the code. How do the times for each operation compare to the times before you optimized? What do you conclude?

  8. In your project programs, once they are debugged and running, you'll want to use the -O2 (or maybe -O3) optimization flag.


A "profiling" tool, such as gprof, allows you to analyze how the execution time of your program is divided among various function calls. This information identifies the candidate sections of code for optimization. (You don't want to waste time optimizing a part of the code that is only active for 1% of the run time!) We'll use the eigen_basis_class.cpp code from an earlier session as a guinea pig.

  1. To use gprof, compile and link the relevant codes with the -pg option. You can do this most easily by editing make_eigen_basis_class and adding -pg to BOTH the CFLAGS and LDFLAGS lines. (Note that make_eigen_basis_class has $(MAKEFILE) added to several lines to ensure that the codes are recompiled if the makefile changes.)
  2. Execute the program as usual (choose a fairly large basis size so that it takes a while to execute, building up statistics), which generates a file called gmon.out that is used by gprof. The program has to exit normally (e.g., you can't stop it with control-c) and any existing gmon.out file will be overwritten.
  3. Run gprof and save the output to a file (e.g., gprof.out):
    gprof eigen_basis_class > gprof.out
    Edit gprof.out and try to figure out from the "Flat profile" and the explanations where (i.e., in what functions) the program spends the most time. What are the most time-consuming functions? Would you try to speed up the part that finds the eigenvalues?

  4. Now change the makefile so that -O3 optimization is used. Run gprof and save the output to a new file:
    gprof eigen_basis_class > gprof2.out
    Compare gprof.out and gprof2.out. What has -O3 done for you?

Revisiting area.cpp with a C++ Class

Our first simple C++ code from Session 1 used procedural programming, with the focus on the formula (which would typically be coded as a function) for calculating the area. Now we revisit it from an object oriented perspective.

  1. Look at the printout with test_Circle.cpp and the old area.cpp, with the Circle class defined on the back. Note how all of the details of the area calculation are now hidden. Any questions on the Class definition? Predict where in the test_Circle.cpp code the two circles will be created and where they will be destroyed. Why do we define get_radius and set_radius methods?

  2. Using make_test_Circle, compile and link test_Circle.cpp with the class files. Run it and note where the circles are created and destroyed. Do you understand why they are destroyed in this order?

  3. Add a method to Circle.cpp (and Circle.h!) so that we can get the circumfrence from my_circle.circumfrence(). Try it out in test_Circle.cpp. Did you succeed?

  4. Take a look at the private_vs_public.cpp code, which is a self-contained class and main program. Notice how the main program can access and change the x value and use the xsq function. But try changing from x to y in the statements getting, printing, and changing the value of x. What happens? Why does the get_y function work?

  5. (Extra, only do this if you are fast.) Make a Sphere class with Sphere.cpp and Sphere.h, and try it out by adding to test_Circle.cpp to create a Sphere and print out its radius. Did you succeed?

Introduction to Rsync

Rsync is a backup or mirroring tool that only copies the differences of files that have changed. It can do this transfer in compressed form and using ssh for security (both recommended!). So updates are fast, efficient, and secure. Here we'll make a trial run so that you get the basic idea.

  1. [The following is from Session 5.] Bash (tcsh) aliases. The .bashrc (.cshrc.more) file sits in your home directory and is "sourced" when you start an interactive terminal. Take a look at the sample .bashrc (.cshrc.more) file printout (these files, without the ".", are in the session 9 zip file). Copy any aliases of interest to your own .bashrc (.cshrc.more) or the whole thing using cp bashrc ~/.bashrc (cp cshrc.more ~/.cshrc.more) if you don't have one already. Activate the changes with the command: source ~/.bashrc (source ~/.cshrc).
  2. Take a look in the bashrc or cshrc.more file for the definitions "rsyncbackup" and "rsyncmirror". These aliases use some of the common rsync options. The difference is that rsyncmirror deletes files at the destination that don't exist at the source; be careful of this!
  3. Create a directory in your home directory called "780_backup" to use for testing rsync (i.e., "mkdir 780_backup").
  4. We'll create a backup of the session 9 directory on fox. (Since all your files are mounted on all of the physics public computers, you don't need to copy to another computer, but this is a demonstration!) Go to the parent directory of session_09. Give this command:
    rsyncbackup session_09 fox.mps.ohio-state.edu:780_backup
    If you are asked if you want to continue connecting, answer yes. You'll probably be asked for your password as well (it is the same one everywhere). You should see a list of files transferred and some statistics. [If you don't have a Department unix account, backup to your home directory with
    rsyncbackup session_09 ~
    (where ~ is your home directory; to go there use cd ~).]
  5. Check that the directory was transferred. Now go back to the original session_09 directory and change one file. (E.g., edit one of the .cpp files and add a comment.) Then repeat the rsync transfer (from the appropriate directory), using "!rsync" to avoid typing the entire alias. You should find that only the changed file is updated.

780.20: 1094 Session 9. Last modified: 10:38 am, February 13, 2012.