========================================================================== Section 5 Notes ========================================================================== - 1. Using Assertions and Assertion-Based Testing - 2. Using gprof for Profiling - 3. Using valgrind for Dynamic Allocation Analysis * As in the tutorial, log into ecelinux from your laptop % ssh -X cb535@ecelinux-01.ece.cornell.edu % ssh -X cb535@ecelinux-02.ece.cornell.edu - don't forget to source the setup script! % source setup-ece2400.sh * Clone the tutorial repo % mkdir -p $HOME/ece2400 % cd $HOME/ece2400 % git clone git@github.com:cornell-ece2400/ece2400-tut3-c sec5 % cd sec5 % TUTDIR=$PWD - if you have not uploaded your SSH key onto GitHub, then you may need to clone the repo like this: % mkdir -p $HOME/ece2400 % cd $HOME/ece2400 % git clone https://github.com/cornell-ece2400/ece2400-tut3-c sec5 % cd sec5 % TUTDIR=$PWD -------------------------------------------------------------------------- 1. Using Assertions and Assertion-Based Testing -------------------------------------------------------------------------- - we should all know about the ECE 2400 build system % cd $TUTDIR/tut3-gcd % mkdir build % cd build % ../configure % make gcd-test % ./gcd-test % make check - currently gcd will return -1 if the parameters are negative - assume the specification requires that the program immediately fail when gcd is called with negative parameters - we can use assertions to specify conditions which absolutely must be true, and if the condition is false we should abort the program - replace this in tut3-gcd/gcd.c if ( (x < 0) || (y < 0) ) return -1; - with this: assert( (x >= 0) && (y >= 0) ) - need to include the right header file: #include - modify gcd-main.c to call gcd with a negative parameter: int main() { int a = -5; int b = 15; int c = gcd( a, b ); printf( "gcd of %d and %d is %d\n", a, b, c ); return 0; } - compile, build, and run gcd-main % cd $TUTDIR/tut3-gcd/build % make gcd-main % ./gcd-main gcd-main: ../src/gcd.c:16: gcd: Assertion `(x >= 0) && (y >= 0)' failed. Aborted (core dumped) - you can add a more informative message like this: assert( (x >= 0) && (y >= 0) && "Parameters cannot be negative!" ) - try reruning the tests % cd $TUTDIR/tut3-gcd/build % make gcd-test % ./gcd-test boxfort-worker: ../src/gcd.c:16: gcd: Assertion `(x >= 0) && (y >= 0)' failed. [----] ../src/gcd-test.c:56: Unexpected signal caught below this line! [FAIL] gcd::negative: CRASH! - how do we write a test to test the assertion? - how do we write a test that will _pass_ if code asserts? - need to tell Criterion we _expect_ an assertion - http://criterion.readthedocs.io/en/master/starter.html#testing-signals - change the negative test in gcd-test.c like this: Test( gcd, negative, .signal=SIGABRT ) { cr_assert_eq( gcd( -5, 15 ), -1 ); cr_assert_eq( gcd( 5, -15 ), -1 ); cr_assert_eq( gcd( 5, -15 ), -1 ); } - need to also include "signal.h" which defines SIGABRT - now gcd should pass the test because it causes assertion as expected % cd $TUTDIR/tut3-gcd/build % make gcd-test % ./gcd-test - note that assertions are _not_ in the eval build! % cd $TUTDIR/tut3-gcd/build % make gcd-main % ./gcd-main % cd $TUTDIR/tut3-gcd % mkdir build-eval % cd build-eval % ../configure --enable-eval % make gcd-main % ./gcd-main -------------------------------------------------------------------------- 2. Using gprof for Profiling -------------------------------------------------------------------------- - let's change gcd-main.c around a bit int foo() { return 1; } void bar() { for ( int i = 0; i < 10; i++ ) gcd( 100, i ); } int baz() { return gcd( 1e9, 1 ); } int main() { foo(); bar(); baz(); return 0; } - now let's build and run gcd-main % cd $TUTDIR/tut3-gcd/build % make gcd-main % ./gcd-main - we can use gprof to "profile" our code to see which functions are taking the largest portion of our execution time % gprof -b ./gcd-main - the flat profile reports how much time was spent in each function - the call graph profile reports how much time was spen in each function given a specific context for the function call - we can see that + the entire time of the program is spent in the gcd function + gcd was called 11 times + gcd was called 10 times from bar + gcd was called once from baz + gcd calls from bar account for 90% of the execution time + gcd calls from baz account for 10% of the execution time - profiling helps a programmer focus on the "hot" functions - it is obvious we need to improve the performance of gcd - ... but we may also try and reduce the number of calls to gcd in baz - ... or use a different approach in baz -------------------------------------------------------------------------- 3. Using valgrind for Dynamic Allocation Analysis -------------------------------------------------------------------------- - variables can be dynamically allocated on the stack - the programmer is responsible for managing when these variables are deallocated, if they are never deallocated that is a memory "leak" - take a look at the program in $TUTDIR/tut3-mem/leak.c - build and run this program % cd $TUTDIR/tut3-mem % gcc -g -o leak leak.c % ./leak - let's use valgrind to check for memory leaks % cd $TUTDIR/tut3-mem % valgrind --leak-check=yes ./leak - valgrind tells us that 48 bytes have "leaked" - this is because we allocated three nodes but never freed them - every malloc eventually needs a corresponding free before end of prog - add this code after the printf statements and before return 0 - order in which we free the nodes is very important! - once a pointer is free it is "dangling" - dereferencing a dangling pointer can lead to undefined behavior free( n_ptr->next_ptr->next_ptr ); free( n_ptr->next_ptr ); free( n_ptr ); - now rebuild and rerun the program using valgrind % cd $TUTDIR/tut3-mem % gcc -g -o leak leak.c % valgrind --leak-check=yes ./leak - no more leaks! - valgrind can also check for many other errors with dynamic allocation