Compiler "Hints" to speed up code

CS 301 Lecture, Dr. Lawlor

The exact type of variable you use can make a huge difference in how the compiler can speed up your code.  In order of increasing compiler terror:
• Compile-time constants, like the number "6", or an "enum {x=6};", or a "const int x=6;".  The compiler loves these, and can use them to unroll loops, predo "if" statements, transform away array indexing, etc.  It's great!
`// factorial, with constants.  Compiler unrolls the loop.  Takes 3ns.enum {n=8};int fact=1;for (int i=1;i<=n;i++) fact*=i;return fact;`

(Try this in NetRun now!)

• Function arguments (by value) or local variables, like "int x=6;".  The compiler's usually smart enough to figure out when they're really constants.  Locals typically get stored in registers, or transformed away entirely, and only rarely have to live out in memory.
`// factorial, with function parameters.  Loop has to run now.  Takes 11ns.int factorial(int n) {	int fact=1;	for (int i=1;i<=n;i++) fact*=i;	return fact;}int foo(void) {	return factorial(8);}`

(Try this in NetRun now!)

• Caveat: "inline" functions don't cost anything.  In this case, adding "inline" means the compiler can see that n is actually a known-value constant, and so unroll the loop.  If you've got a bunch of little functions, this can be a HUGE win!
`// factorial, with inline function.  Compiler can unroll again!  Takes 3ns.inline int factorial(int n) {	int fact=1;	for (int i=1;i<=n;i++) fact*=i;	return fact;}int foo(void) {	return factorial(8);}`

(Try this in NetRun now!)

• Function parameters passed by reference, values accessed via a pointer, or global variables.  These all have to live most of their lives in memory, since compilers are extremely paranoid about copying these values to registers, in case somebody else changes the in-memory version.  You can help the compiler out by making sure it knows nobody else can change these: mark them as "const", or don't write to any other pointers (that could be aliased to the value) or call any other functions (that could change the value).
`// factorial, with by-reference n and a global access.  Two memory accesses per iteration.  Takes 16ns.int fact=0;int factorial(int &n) {	fact=1;	for (int i=1;i<=n;i++) fact*=i;	return fact;}int foo(void) {	int n=8;	return factorial(n);}`

(Try this in NetRun now!)

• Variables marked "volatile".  This keyword is there for weird things like threads or I/O signal handlers that will magically reset the in-memory copy, and the compiler will honor your demands and always reload the value from memory.
`// factorial, with volatile loop index.  Four memory accesses per iteration.  Takes 26ns.int fact=0;int factorial(int &n) {	fact=1;	for (volatile int i=1;i<=n;i++) fact*=i;	return fact;}int foo(void) {	int n=8;	return factorial(n);}`

(Try this in NetRun now!)

Keywords to calm the compiler:
• "const" means the value won't change.  This is good documentation for programmers, and lets the compiler cache and transform away expressions.
• "inline" makes a function integrate into the call-side code, which usually exposes optimization opportunities.  Best for short functions: this can make the machine code bigger if you inline lots of huge functions.
Keywords to frighten the compiler:
• "volatile" means keep the value in memory, always.  Goodbye registers!
• "extern" means somebody else will be doing something unknown with the value.  Goodbye predictable transformations!
For the compiler, appearance matters!  You know that "sqrt(17)" will never, ever change and only needs to happen once, ever.  But you also know that "print_int(17)" needs to get called every time you write it.  If it's constant, say so!  If it only needs to happen once, only do it once!