GCC local struct vs individual variables and PGO optimziation

Viewed 64

We are trying to refactor the interpreter loop SWI-Prolog. That is a giant function using GCC's label addresses to refer to the virtual machine instructions. This function has a number of VM register variables. It turns out that it makes a difference whether one writes

PL_next_solution(qid_t qid)
{ type1 v1;
  type2 v2;
  // 13 in total

  // lots of code
}

or

PL_next_solution(qid_t qid)
{ struct
  { type1 v1;
    type2 v2;
    // 13 in total
  } registers;

  // lots of code
}

Without profile guided optimization the performance difference of the resulting program is small (<5%) while with PGO optimization the difference is about 20%. This was rather unexpected. Why does this happen? Does gcc respect the memory layout of the structure despite the fact that it isn't known outside this function?

1 Answers

Based on your further tests, it sounds like the performance hit comes from taking the address of one or more of the struct members and passing it to a function that the compiler can't see. I believe that when you do this, the compiler has to assume that the entire struct has escaped. This inhibits many possible optimizations: it means that the struct has to be laid out in memory, and moreover that members have to be stored to and reloaded from that memory around every function call.

To see why, look at an example like this:

void foo(int *);
void bar(void);
struct qux { 
    int a,b,c;
    char huge[5000];
};
int fum(void) {
    struct qux s = { 1,2,3 };
    foo(&s.b);
    s.c=4;
    bar();
    return s.a+s.c;
}

Try on godbolt

Notice that even with maximum optimization:

  • the unused member huge is still allocated and initialized
  • s.c=4 involves a store to memory
  • s.a and s.c are reloaded from memory after the call to bar()

I think the point is that for all the compiler knows, the functions foo() and bar() could be doing:

int *global_ptr;
void foo(int *ip) {
    struct qux *qp = (struct qux *)((char *)ip - offsetof(struct qux, b));
    global_ptr = &qp->c;
    printf("%d %d\n", qp->a, qp->huge[1234]);
}

void bar(void) {
    *global_ptr = 37;
}

I believe such code would be well-defined; foo must print out 1 0 and fum must return 38. Of course, the programmer needs to ensure that foo is only ever called with the address of the b member of a struct qux, and that bar() isn't called again after fum returns, because of the dangling pointer - but if they do so then the code should work.

On the other hand, if you change the call in fum to foo(NULL), you can see that everything vanishes: s.huge is never initialized nor even allocated, s.b disappears entirely, s.a and s.c are optimized away, and return s.a+s.c; is constant-folded into return 5;. In this case the compiler can be sure that the struct never "escapes" and so it can be optimized as it pleases.

Related