As the weak-linked functions are globally visible, the compiler cannot omit the function calls, but since they are in the same scope, it may assume that because the functions do nothing, the call can be reordered wrt. variable accesses, unless explicit compiler barriers are used.
That doesn't sound right to me. And if right then it's awful!
Here, the "may" refers to practical reality, and not that it is allowed by the C standard.
If a compiler assumes the variable access/caching can be reordered wrt. to such a function call, I do consider it an optimization bug.
I did check a few cases at godbolt.org, and could not reproduce the bad behaviour with GCC, Clang, or ICC on x86-64, rv32gc, or rv64gc. They always do the proper variable accesses before the function call, sometimes leaving the values in registers doing any arithmetic with the registers after the function call, which is perfectly okay.
So, I cannot be certain if I recall correctly and have seen it happen, or just recall incorrectly.
Also, while GCC, Clang, and ICC do, technically the C compiler doesn't know that the runtime functions can be replaced because that (and symbol
weakness) is outside the scope of the C standard, and is just a "linker trick".
(One technically can modify the symbol linkage directly in the ELF object file, for example via
objcopy --globalize-symbol=symbolname, but that is dangerous because the compiler can make assumptions about local visibility objects (
static) it cannot for global visibility objects, and will optimize code differently.)
The practical effect of what I described is that if one uses GCC/Clang/ICC extended inline assembly to implement the disable/restore interrupt functions, ie.
static inline UINT disable_interrupts(void) {
UINT oldmask;
asm volatile ( "..ARCH-SPECIFIC-ASSEMBLY.."
: "..REGISTERSPEC.." (oldmask)
: /* no inputs */
: "memory" );
return oldmask;
}
static inline void restore_interrupts(UINT mask) {
asm volatile ( "..ARCH-SPECIFIC-ASSEMBLY.."
: /* no outputs */
: "..REGISTERSPEC.." (mask)
: "memory" );
}
the
"memory" extended asm clobber is essential, as it turns the inline assembly statement (often just one or two instructions) into a compiler barrier also.
Leave it out at your own peril. With it included, the behaviour is what one would expect, and generates no unnecessary or extra code, and does not harm desired optimizations at all.
With other C compilers, something like a
__COMPILER_BARRIER() macro (specific to that compiler!) must be added into the function bodies before and after accessing the interrupt mask, to ensure correct ordering with respect to all other operations.
One discovers these kinds of details not via debugging per se, but by carefully observing the actual generated assembly code, and comparing that to the developer requirements and expectations. Fortunately,
reading assembly is much easier than writing fast/efficient assembly, so I do consider it a necessary skill for any low-level/embedded C developer.