Generic programming with preprocessor macros.
authorClaude Heiland-Allen <claude@mathr.co.uk>
Sat, 21 Dec 2013 00:41:12 +0000 (00:41 +0000)
committerClaude Heiland-Allen <claude@mathr.co.uk>
Sat, 21 Dec 2013 00:41:12 +0000 (00:41 +0000)
commit4ecccb39c916dca81a60860029b23aeb62a2a1b7
tree7482bd7cf7b9bcdc731e1a859a3aaf241e25e487
parentecb18f012205e586c683a15c5829d71d06dcdb32
Generic programming with preprocessor macros.

In supporting double precision to allow deeper zooming without pixelation,
we duplicated a lot of code, just to change the number type used.  Now we'll
get rid of the code duplication, leaving us with one implementation of each
algorithm.  We move one copy of the implementation into mandelbrot_imp.c,
using the name FTYPE wherever ``float'' or ``double'' is needed.  We also
wrap each function that has variants for different number types in FNAME().

Now, FTYPE and FNAME() are not defined by the C standard, so the code won't
compile.  We'll define them ourselves, in a header file mandelbrot.h.  This
begins and ends with an idiomatic header guard, to avoid errors if the file
would be included more than once.  Next it includes the library headers that
mandelbrot_imp.c needs.  We to define FTYPE to be a floating point type,
like float, whose variant function suffix is f.  Then we include the
implementation code.  Now we can undefine FTYPE and FNAME(), and repeat this
stanza for the other floating types available in C: double and long double.

This technique uses the C preprocessor, which the compiler runs early on in
the compilation pipeline (first the preprocessor performs file inclusion and
macro expansion, then the C compiler generates assembly source for the
target architecture, which the assembler translates to machine code, which
is then linked into a program).  Macro expansion is similar to search and
replace within a text editor, with macros able to take parameters.
The ## operator in a macro joins two tokens together to form a new token.

Now our main program file mandelbrot.c includes our header file, and its
main() parses the command line arguments and calls the appropriate function
from our mini-library.  We use strtold() now instead of atof() to avoid
losing precision before we even begin.  The switch() statement is an
alternative to nested if statements.  Finally we add the extra files to the
prerequisites of our Makefile target, so that ``make'' will rebuild the
program when we update any of them.

Now we can benchmark the implementation with different number types:

    $ time ./mandelbrot 0 0 2 0 > float.pgm
    real    0m0.296s
    user    0m0.288s
    sys     0m0.004s
    $ time ./mandelbrot 0 0 2 1 > double.pgm
    real    0m0.130s
    user    0m0.128s
    sys     0m0.000s
    $ time ./mandelbrot 0 0 2 2 > long-double.pgm
    real    0m0.525s
    user    0m0.528s
    sys     0m0.000s
    $

These results are surprising: float is actually slower than double,
despite having less precision.
Makefile
mandelbrot.c
mandelbrot.h [new file with mode: 0644]
mandelbrot_imp.c [new file with mode: 0644]