Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loop unrolling in inlined functions in C

I have a question about C compiler optimization and when/how loops in inline functions are unrolled.

I am developing a numerical code which does something like the example below. Basically, my_for() would compute some kind of stencil and call op() to do something with the data in my_type *arg for each i. Here, my_func() wraps my_for(), creating the argument and sending the function pointer to my_op()... who’s job it is to modify the ith double for each of the (arg->n) double arrays arg->dest[j].

typedef struct my_type {
  int const n;
  double *dest[16];
  double const *src[16];
} my_type;

static inline void my_for( void (*op)(my_type *,int), my_type *arg, int N ) {
  int i;

  for( i=0; i<N; ++i )
    op( arg, i );
}

static inline void my_op( my_type *arg, int i ) {
  int j;
  int const n = arg->n;

  for( j=0; j<n; ++j )
    arg->dest[j][i] += arg->src[j][i];
}

void my_func( double *dest0, double *dest1, double const *src0, double const *src1, int N ) {
  my_type Arg = {
    .n = 2,
    .dest = { dest0, dest1 },
    .src = { src0, src1 }
  };

  my_for( &my_op, &Arg, N );
}

This works fine. The functions are inlining as they should and the code is (almost) as efficient as having written everything inline in a single function and unrolled the j loop, without any sort of my_type Arg.

Here’s the confusion: if I set int const n = 2; rather than int const n = arg->n; in my_op(), then the code becomes as fast as the unrolled single-function version. So, the question is: why? If everything is being inlined into my_func(), why doesn’t the compiler see that I am literally defining Arg.n = 2? Furthermore, there is no improvement when I explicitly make the bound on the j loop arg->n, which should look just like the speedier int const n = 2; after inlining. I also tried using my_type const everywhere to really signal this const-ness to the compiler, but it just doesn't want to unroll the loop.

In my numerical code, this amounts to about a 15% performance hit. If it matters, there, n=4 and these j loops appear in a couple of conditional branches in an op().

I am compiling with icc (ICC) 12.1.5 20120612. I tried #pragma unroll. Here are my compiler options (did I miss any good ones?):

-O3 -ipo -static -unroll-aggressive -fp-model precise -fp-model source -openmp -std=gnu99 -Wall -Wextra -Wno-unused -Winline -pedantic

Thanks!

like image 273
FiniteElement Avatar asked Jun 12 '15 11:06

FiniteElement


People also ask

Can we use loop in inline function in C++?

A function with loops can be made inline, But every time a function is called, there is a certain amount of performance overhead that occurs.

What is Loop unrolling in C?

Loop unrolling in C code When a loop is unrolled, the loop counter requires updating less often and fewer branches are executed. If the loop iterates only a few times, it can be fully unrolled so that the loop overhead completely disappears. The compiler unrolls loops automatically at -O3 .

What is the use of loop unrolling?

Loop unrolling is a technique used to increase the number of instructions executed between executions of the loop branch logic. This reduces the number of times the loop branch logic is executed.

What is Pragma loop unrolling?

The UNROLL pragma specifies to the compiler how many times a loop should be unrolled. The UNROLL pragma is useful for helping the compiler utilize SIMD instructions. It is also useful in cases where better utilization of software pipeline resources are needed over a non-unrolled loop.


2 Answers

Well, obviously the compiler isn't 'smart' enough to propagate the n constant and unroll the for loop. Actually it plays it safe since arg->n can change between instantiation and usage.

In order to have consistent performance across compiler generations and squeeze the maximum out of your code, do the unrolling by hand.

What people like myself do in these situations (performance is king) is rely on macros.

Macros will 'inline' in debug builds (useful) and can be templated (to a point) using macro parameters. Macro parameters which are compile time constants are guaranteed to remain this way.

like image 86
egur Avatar answered Oct 12 '22 11:10

egur


It's faster, because your program does not assign memory to the variable.

If you don't have to perform any operations on unknown values they are treated as if they were #define constant 2 with type checking. They are just added while the compilation.

Could you please chose one of the two tags (I mean C or C++), it's confusing, because the languages treat const values differently - C treats them like normal variables which value just can't be changed, and in C++ they do or don't have memory assigned depending on the context (if you need their address or if you need to compute them when the program is running, then memory is assigned).

Source: "Thinking in C++". No exact quote.

like image 44
Adrian Jałoszewski Avatar answered Oct 12 '22 12:10

Adrian Jałoszewski