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.