Conditional Compilation in C for getting different versions of one function

3.2k views Asked by At

I asked myself if there is a nice way to get different versions of one function without copying the whole source code. I would like to have different versions one the one hand for measuring the execution time and on the other hand for writing some intermediate result (for analytic purpose).

Let me explain it by this example: I am writing a iterative solver for solving a linear system, in my case the GMRES algorithm. It would like to measure the runtime in one run and to write the residual at every iteration step in a different run. The structure of my program looks like:

//main.c

#include "gmres.h"

// setup arrays
flag_print_res = 0;
GMRES(A,x,b,tol,maxtiter,flag_print_res);
// reset arrays
flag_print_res = 1;
GMRES(A,x,b,tol,maxtiter,flag_print_res);
//..

and the function:

//GMRES.c

void GMRES(double* A, double *x, double *b, double tol, int maxiter, int flag_print_res)
{
    // …
    start_time = ((double) clock ())/CLOCKS_PER_SEC;
    for(iter=0; iter<maxiter; iter++)
    {
        // ...
        if(flag_print_res)
        {
            fprintf(file, "%d %13.5e\n", iter, res);
        }
        // ...
    }
    end_time = ((double) clock ())/CLOCKS_PER_SEC;
    printf("execution time = %13.5e\n", end_time-start_time);
}

However, the problem for the execution time is, that evaluation the if statement costs time, specially when it is evaluated every iteration. Therefore my question: How can I create different version of the function, in which the if statement is evaluated at compile time?

3

There are 3 answers

9
Ely On

Use directives. With a directive you can instruct the C pre-processor to either print or not to print.

It is a common practice for debugging by the way. You can, at compile time, have only one decision being made. (You see below I put #define and #undef in the same source file. The decision is at compile time, so you can use either one or the other. I hope you get the idea. You need to compile twice if you want to execute the two version of your program.)

When invoking your program you can add a directive as parameter to the (gcc?) compiler, so you don't need to modify your source code every time you want to switch the directive (e.g. gcc -D FLAG_PRINT_RES ....). Below is showing the two poossibilities.

//main.c

#include "gmres.h"

// setup arrays

================
#define FLAG_PRINT_RES;  <-- flag is set. when you compile the code fprintf below is compiled
GMRES(A,x,b,tol,maxtiter);
=======OR=======
#undef FLAG_PRINT_RES; <-- flag is not set. when you compile the code fprintf below is not compiled
GMRES(A,x,b,tol,maxtiter);
================

// reset arrays
//..

And make use of the flag in case it is defined.

//GMRES.c

void GMRES(double* A, double *x, double *b, double tol, int maxiter)
{
    // …
    start_time = ((double) clock ())/CLOCKS_PER_SEC;
    for(iter=0; iter<maxiter; iter++)
    {
        // ...
        #ifdef FLAG_PRINT_RES <-- if flag is set then compile the next line of code
            fprintf(file, "%d %13.5e\n", iter, res);
        #endif
        // ...
    }
    end_time = ((double) clock ())/CLOCKS_PER_SEC;
    printf("execution time = %13.5e\n", end_time-start_time);
}

Note that it is a pre-processor (thus before compile time decision). You cannot have a dynamic approach at runtime in C without using function pointers, at least not as far as I know.

If you'd use C++ you might find a run time solution (you can overload methods/functions).

If you allow to change your code more or less considerably, you might be able to find a solution in C using function pointer. However, it is too much effort for me to set up an example, I'll show you the basics in the Appendix below. And I am not sure if it is better than the if statement, because each time a function is called a context is built and destroyed after return. However if you add more print statements then a function pointer could be a viable solution for you.

So here is the summary:

  • Just leave the if statement
  • Use the pre-processor to compile conditionally
  • Use function pointer to make a run time decision whether to print or not

Appendix (function pointer)

#include <stdio.h>

int (*p)(const char *format, ...);                           <-- function pointer to printf(...) library function
int p_null(const char *format, ...);                         <-- function of identical signature to printf, but doing nothing
void f(int (*p)(const char *format, ...), const char *text); <-- a function receiving a function pointer and optionally other parameters

int main() {

    p = &printf;  <-- point to printf library function
    f(p, "FLAG"); <-- will print FLAG in console
    p = &p_null;  <-- point to dummy function doing nothing
    f(p, "FLAG"); <-- will not print FLAG in console

    return 0;
}

// dummy function doing nothing
int p_null(const char *format, ...) {;}

// function call receiving function pointer
void f(int (*p)(const char *format, ...), const char *text) {
    (*p)("TEST");
}
1
Mabus On

You can make it so that flag_print_res is a macro, known at compile time. If the compiler know at compile time that the condition of an if is false, and you have optimizations enabled, it will remove the if (if it's a good compiler).

Then you can create two versions of your program in the makefile, one with the define true and other with the define false, so one program measures the runtime and the other don't.

0
M Oehm On

You can control settings at compile time with macros. They are usually used to make only one version of the function available in a build, however.

If you want to have both versions available in the same program, you must implement the two different versions of the function. The preprocessor can help you to avoid duplicating the code.

Multiple inclusion of separate implementation

One approach is to implement the function in a separate file gmres.c:

#ifndef GMRES
#error "GMRES not defined"
#endif

void GMRES(double* A, double *x, double *b, double tol, int maxiter)
{
    for (iter=0; iter < maxiter; iter++)
    {
        // ...

        #ifdef GMRES_PRINT
        fprintf(file, "%d %13.5e\n", iter, res);
        #endif

        // ...
    }
}

And then include that file with two sets of macros:

#define GMRES gmres            // plain version
#include "gmres.c"
#undef GMRES

#define GMRES gmres_print     // printing version
#define GMRES_PRINT           // set print flag
#include "gmres.c"
#undef GMRES

Although #include is commonly used to include header files, it can be used to include any file, and here it makes sense to include a *.c file. Make sure you don't have #pragma once or a similar setting active. The #error directive ensures that the name of the function is given as a macro.

Compile separate objects

The same can be achieved by compiling the same source file into two separate objects:

cc -DGMRES=gmres -o gmres.o -c gmres.c
cc -DGMRES_PRINT -DGMRES=gmres_print -o gmres_print.o -c gmres.c

cc -o gmres gmres.o gmres_print.o ...

Here, the versions are controlled via the build control tool, e.g. Makefile. You must make sure that there are no duplicate symbols.

Cheap template

Another approach is a "cheap template" to implement the whole function as macro:

#define GMRES_IMPL(NAME, FLAG)                                       \
void NAME(double* A, double *x, double *b, double tol, int maxiter)  \
{                                                                    \
    for (iter=0; iter < maxiter; iter++)                             \
    {                                                                \
        // ...                                                       \
                                                                     \
        if (FLAG) fprintf(file, "%d %13.5e\n", iter, res);           \
                                                                     \
        // ...                                                       \
    }                                                                \
}

and then implement two versions of it with different parameters:

GMRES_IMPL(gmres, 0)
GMRES_IMPL(gmres_print, 1)

You cannot have preprocessor directives in a macro, so control must be via macro arguments. The compiler can evaluate constant expressions like if (0) and if (1) and eliminate dead code accordingly.

This approach may have its uses, but has one major drawback: All error messages and debugging symbols refer to the line where the function implementation is evoked with GMRES_IMPL, which makes finding errors very hard.