I know that there is theoretically another level of indirection when accessing them,
Well it depends, a really dumb compiler may introduce extra levels of indirection for a structure, but a smart compiler can actually produce better code if a structure is involved.
Contrary to other answers the compiler DOES NOT know the address of global variables. For global variables in the same compilation unit it knows their location relative to each other, but not their absolute location. For variables in other compilation units it doesn't know anything about their location in either absolute or relative terms.
Instead the code generated by the compiler will have placeholders for the locations of the global variables, those will then be replaced by actual locations when the final address is determined (traditionally when the program is linked, but sometimes not until it is run).
Furthermore, while there are some CPUs (like the 6502) that can access a global memory location directly without any prior set-up there are many that can't. On arm for example, to access a global variable the compiler must typically first load the address of the variable into a register, either using a literal pool or, on more recent versions of arm, using movw/movt. Then access the global variable using a register-relative load instruction.
This means for variables outside the compilation unit accessing multiple elements of the same global structure is likely more efficient than accessing individual global variables.
To test this I fed the following code into godbolt with ARM gcc 8.2 and -O3 optimization.
extern int a;
extern int b;
struct Foo {
int a;
int b;
};
extern struct Foo foo;
void f1(void) {
a = 1;
b = 2;
}
void f2(void) {
foo.a = 1;
foo.b = 2;
}
This resulted in
f1:
mov r0, #1
mov r2, #2
ldr r1, .L3
ldr r3, .L3+4
str r0, [r1]
str r2, [r3]
bx lr
.L3:
.word a
.word b
f2:
mov r1, #1
mov r2, #2
ldr r3, .L6
stm r3, {r1, r2}
bx lr
.L6:
.word foo
In the case of f1 we see that the compiler loads the addresses of a and b separately from literal pools (the addresses in the literal pools will be filled in later by the linker).
However in the case of f2 the compiler only has to load the address of the structure from the literal pool once, better still because it knows the two variables are next to each other in memory it can write them with a single "store multiple" instruction rather than two separate store instructions.
but from a style POV is this better or worse than putting them in files globals.c and/or globals.h?
IMO that depends if the variables are actually related or not.