# Measuring Performance

CS 641 Lecture, Dr. Lawlor

## When You Care about Performance

There are times when you don't care how fast your code runs.  Generally, a human being can't tell the difference between "this code responds in under 1 millisecond" and "this code responds in under 1 microsecond"; but a human *can* tell the difference between "here's an Excel spreadsheet and some perl scripts that will do the job" and "I've got a very fast C++ version that's almost working, just give me another week or two".  In other words, it probably doesn't matter if the once-a-week payroll processing takes two seconds or two hours to complete, as long as it's automated and it works.  Generally speaking,
• Even if you're sitting there waiting for it, a few seconds or less is usually "fast enough".  The more rarely you have to do it, the slower it can be (think of rebooting, burning DVDs, etc.)
• If it runs at midnight, as long as it finishes by the next morning, that's usually fast enough.
But, there are also times when you do care about how fast your code runs.  For example, if you're writing "real-time" control code such as for an autonomous robotic car's braking system, then your code *can't* take two minutes to figure out that it's time to hit the brakes--the car will have already crashed by that point!  If you're writing a web server, your code *can't* take two minutes to respond to a web server request--the person sitting in front of the web browser will get bored and leave.  If you're writing the physics simulation or rendering code for a game, if your code takes even 1/10 of a second to respond to events, the game will feel noticably "laggy"; if it takes a second or more, it's not a game anymore, it's powerpoint!

## Performance-Increasing Algorithm

Here's my general-purpose algorithm for increasing code performance:
1. Figure out what's slow.
2. Speed up the slow parts.
Part 1 is tricky, so people usually skip directly to part 2 with some random guess as to what's slow.  Occasionally, through sheer random chance, they'll get lucky and speed up the slow part of the code.  Usually, folks waste huge amounts of effort trying to speed up the wrong thing, get near-zero performance improvement, and give up.

For example, say a program consists of three parts: A, B, and C.  Part B takes an hour.  A and C take ten seconds each.  The program overall thus takes one hour twenty seconds.  If you spend a year working on A, and make it 10x faster, you've saved nine seconds and the program overall will take one hour eleven seconds--who cares!   If you spend a hour working on B, and make it 10% faster, you've saved six minutes, and the program now takes 54 minutes.  So speed up the slow parts first!

## Figuring Out What's Slow (That is, Performance Analysis)

Your basic tool for figuring out what's slow is a timer.  This is just a function that returns the current real time ("wall-clock" time).   NetRun has a builtin function called "time_in_seconds", which returns a double giving the number of seconds.  Here's the implementation (from project/lib/inc.c):
`/** Return the current time in seconds (since something or other). */#if defined(WIN32)#  include <sys/timeb.h>double time_in_seconds(void) { /* This seems to give terrible resolution (60ms!) */        struct _timeb t;        _ftime(&t);        return t.millitm*1.0e-3+t.time*1.0;}#else /* UNIX or other system */#  include <sys/time.h> //For gettimeofday time implementationdouble time_in_seconds(void) { /* On Linux, this is microsecond-accurate. */        struct timeval tv;        gettimeofday(&tv,NULL);        return tv.tv_usec*1.0e-6+tv.tv_sec*1.0;}#endif`
(As usual, there's one version for Windows, and a different version for everything else.)

The usual way to use a timer like this is to call the timer before your code, call it after your code, and subtract the two times to get the "elapsed" time:
`#include <fstream>int foo(void) {	double t_before=time_in_seconds();		std::ofstream f("foo.dat");  /* code to be timed goes here */	double t_after=time_in_seconds();	double elapsed=t_after - t_before;	std::cout<<"That took "<<elapsed<<" seconds\n";	return 0;}`
The problems are that:
1. time_in_seconds returns seconds, but it's only accurate to microseconds (not nanoseconds!)
2. time_in_seconds takes about 800ns to return (which is considered pretty fast!)
3. Your code might get interrupted by the OS, other processes, hardware events, etc
Problems 1 and 2 can be cured by running many iterations--"scaling up" many copies of the problem until it registers on your (noisy, quantized) timer.  So instead of doing this, which incorrectly claims "x+=y" takes 700ns:
`int x=3, y=2;double t1=time_in_seconds();x+=y;double t2=time_in_seconds();std::cout<<(t2-t1)*1.0e9<<" ns\n";return x;`

(Try this in NetRun now!)

You'd do this, which shows the real time, 0.5ns!
`int x=3, y=2, n=1000000;double t1=time_in_seconds();for (int i=0;i<n;i++) x+=y;double t2=time_in_seconds();std::cout<<(t2-t1)/n*1.0e9<<" ns\n";return x;`

Problem 3 can be cured by running the above several times, and throwing out the anomalously high values (which were probably interrupted).

Your other main tool for performance analysis is a "profiler".   This is a library that keeps track of which function is running, and totals up the number of function calls and time spent in each one.  The "Profile" checkbox in NetRun will run a profiler, and show you the top functions it found in your code.

Both timing and profiling have some problems, though:
• Timing and profiling both take time themselves, which skews your measurement of the code.  This is often amusingly refered to as a Heisenberg effect--stuff behaves differently when you're watching it!
• Timing and profiling both fail on very short pieces of code.  Sometimes you'll have to repeat the code many times for it to take a measureable length of time.
NetRun has a nice little built-in function called print_time that takes a string and a function.  It times the execution of (many iterations of) the function, then divides by the number of iterations to print out exactly how long each call to the function takes.  We'll be using print_time below to measure the speed of various operations.

## Speeding It Up

The biggest gains in performance come from taking CS 311, the algorithms class.  I've actually personally worked hard on speeding up O(n^2) algorithms, and been happy with a 5x performance increase, until I realized there's an O(n lg(n)) algorithm which just instantly gave a 1000x performance increase!   O(n^2) is literally 30,000 times slower than O(n lg(n)) for n=1 million (and similar constants).  There's just no way to beat that kind of performance boost!

However, knowing a bit of assembly language can help you speed up your code as well.  Here are a few functions that time various assembly language instructions.
`#include <fstream>int a=10,b=11;double x=2.3, y=3.4;int array[100];int fn_constant(void) {return 0;}int fn_mov(void) {return a;}int fn_index(void) {return array[a];}int fn_add(void) {return a+b;}int fn_sub(void) {return a-b;}int fn_not(void) {return ~a;}int fn_and(void) {return a&b;}int fn_or (void) {return a|b;}int fn_xor(void) {return a^b;}int fn_shl(void) {return a<<b;}int fn_shr(void) {return a>>b;}int fn_addshr(void) {return (a+b)>>b;}int fn_mul(void) {return a*b;}int fn_mad(void) {return a*b+a;}int fn_div(void) {return a/b;}int fn_call(void) {return fn_constant();}int fn_fmov(void) {return (int)(x);}int fn_fadd(void) {return (int)(x+y);}int fn_fmul(void) {return (int)(x*y);}int fn_fmad(void) {return (int)(x*y+x);}int fn_fdiv(void) {return (int)(x/y);}int fn_sqrt(void) {return (int)sqrt(x);}int fn_cos(void) {return (int)cos(x);}int fn_malloc(void) {free(malloc(4)); return 0;}int fn_new(void) {delete new int; return 0;}int fn_vector(void) {std::vector<int> v(a,3); return 0;}int fn_timer(void) {return (int)time_in_seconds();}int fn_ofstream(void) {std::ofstream f("foo.dat"); return 0;}int foo(void) {	print_time("constant",fn_constant);	print_time("index",fn_index);	print_time("mov",fn_mov);	print_time("add",fn_add);	print_time("sub",fn_sub);	print_time("not",fn_not);	print_time("and",fn_and);	print_time("or",fn_or);	print_time("xor",fn_xor);	print_time("shl",fn_shl);	print_time("shr",fn_shr);	print_time("addshr",fn_addshr);	print_time("mul",fn_mul);	print_time("mad",fn_mad);	print_time("div",fn_div);	print_time("fmov",fn_fmov);	print_time("fadd",fn_fadd);	print_time("fmul",fn_fmul);	print_time("fmad",fn_fmad);	print_time("fdiv",fn_fdiv);	print_time("call",fn_call);	print_time("sqrt",fn_sqrt);	print_time("cos",fn_cos);	print_time("malloc",fn_malloc);	print_time("new",fn_new);	print_time("vector",fn_vector);	print_time("timer",fn_timer);	print_time("ofstream",fn_ofstream);	return 0;}`

Here's the output of this program on various machines.  Times are reported in nanoseconds, billionths of a second (1 ns = 10-9 seconds).
 x86-32: Intel Pentium 4, 2.8GHz ia64: Itanium, 800MHz PowerPC: 604e, 200MHz `constant: 5.33 ns/callindex: 5.17 ns/callmov: 5.09 ns/calladd: 5.16 ns/callsub: 5.19 ns/callnot: 5.12 ns/calland: 5.16 ns/callor: 5.11 ns/callxor: 5.16 ns/callshl: 5.34 ns/callshr: 5.12 ns/calladdshr: 5.05 ns/callmul: 5.13 ns/callmad: 5.56 ns/calldiv: 16.78 ns/callfmov: 5.14 ns/callfadd: 8.99 ns/callfmul: 9.33 ns/callfmad: 10.92 ns/callfdiv: 16.52 ns/callcall: 9.42 ns/callsqrt: 17.28 ns/callcos: 70.73 ns/callmalloc: 63.86 ns/callnew: 83.28 ns/callvector: 161.34 ns/calltimer: 814.82 ns/callofstream: 13558.66 ns/call` `constant: 45.14 ns/callindex: 48.92 ns/callmov: 46.42 ns/calladd: 45.16 ns/callsub: 45.15 ns/callnot: 55.17 ns/calland: 45.15 ns/callor: 45.17 ns/callxor: 45.14 ns/callshl: 45.14 ns/callshr: 48.90 ns/calladdshr: 55.18 ns/callmul: 65.22 ns/callmad: 66.47 ns/calldiv: 133.09 ns/callfmov: 61.46 ns/callfadd: 76.61 ns/callfmul: 68.96 ns/callfmad: 68.99 ns/callfdiv: 132.03 ns/callcall: 54.99 ns/callsqrt: 141.82 ns/callcos: 156.80 ns/callmalloc: 421.63 ns/callnew: 441.96 ns/callvector: 371.20 ns/calltimer: 990.96 ns/callofstream: 27243.05 ns/call` `constant: 100.11 ns/callindex: 115.05 ns/callmov: 100.11 ns/calladd: 105.12 ns/callsub: 105.12 ns/callnot: 105.13 ns/calland: 105.13 ns/callor: 105.10 ns/callxor: 105.12 ns/callshl: 105.12 ns/callshr: 105.12 ns/calladdshr: 105.10 ns/callmul: 105.15 ns/callmad: 110.05 ns/calldiv: 195.22 ns/callfmov: 155.09 ns/callfadd: 175.23 ns/callfmul: 175.11 ns/callfmad: 175.20 ns/callfdiv: 320.56 ns/callcall: 165.10 ns/callsqrt: 781.37 ns/callcos: 910.89 ns/callmalloc: 1120.97 ns/callnew: 1245.60 ns/callvector: 617.67 ns/calltimer: 2903.34 ns/callofstream: 27621.16 ns/call`
Here's what we can see generally:
• Almost every integer arithmetic operation (+-*&|^~<<>>) takes almost exactly the same amount of time--under half a nanosecond on typical x86 machines!  In fact, it's difficult to measure the cost of a single arithmetic operation.
• Array indexing is often very fast, under half a nanosecond.
• The one exception is that divides are pretty slow--about 10ns!  This is way more than any other integer arithmetic operation.
• Calling a function is pretty slow; about 5ns.
• Floating point is a little more expensive than integer, a few nanoseconds per operation.  Floating point divides and square roots are expensive, just like integer.  Floating-point cosine is even more expensive--70ns!
• Memory allocation, whether via malloc/free, new/delete, or std::vector, is very expensive--around 100ns!
• Timer (and other operating system) calls are yet more expensive--800ns!
• File operations are insanely expensive--tens of thousands of nanoseconds!
• With slower-clock-rate CPUs, times of most operations scale up close to linearly (10x slower clock rate -> about 10x longer time).
In general, you can often replace expensive operations with cheaper operations.  For example, instead of doing a divide (10ns), you can do an integer bit shift (<1ns) or a floating-point multiply by reciprocal (x/y = x*(1.0/y)).  Instead of making a function call, use "inline" to have the compiler insert the machine code into the caller. Instead of calling "cos", you might eliminate the operation with an algebraic substitution (there are hundreds of useful trig identities), or replace it with a table lookup (since array indexing is very fast).  Instead of calling new/delete every iteration, you might reuse the allocated buffer and postpone the delete to the end of the program.  And anything you can do to avoid opening and closing files repeatedly will dramatically improve performance!