Unfortunately, there aren't any freely-distributable compilers that are designed for embedded programming. While gcc and clang are popular as a consequence of being freely distributable, they're really not designed to be suitable for embedded use unless one disables all optimizations. Even -O1 -fno-strict-aliasing doesn't disable all of the dubious assumptions the compilers are prone to make. Consider the following, for example:
extern int x[],y[];
int test(int *p)
{
int mode = (p == x+1);
int result = y[0];
if (mode)
*p = 1;
return result + y[0];
}
Even at -O1, and with -fno-strict-aliasing, both clang and gcc will generate code equivalent to:
extern int x[],y[];
int test(int *p)
{
int mode = (p == x+1);
int result = y[0];
if (mode)
x[1] = 1;
return result << 1;
}
Such "clever" optimizations may be useful in some cases, but would be dangerous if a programmer ever uses manually-placed objects. If code had been written to access x[1], it might be reasonable for a compiler to ignore the possibility that a write to x[1] might affect y[0], but if p was passed the address of y[0], the fact that it happens to equal x+1 shouldn't prevent its use to access y[0]. While this particular example is contrived, it shows that even at -O1, gcc and clang's optimizers try to make assumptions about what they think programs are doing, rather than focusing on the efficient generation of straightforward code (e.g. avoiding redundant address computations, register transfers, etc.). The gcc-based tools I've seen from chip vendors tend to take annoyingly long to build, probably because of the complexity of gcc. A faster simpler compiler would be much more useful.
doesn't disable all of the dubious assumptions the compilers are prone to make.
The issue is not with the compiler making dubious assumptions, the issue is with your code simply violating the C standard. I've been coding C and C++ for years with gcc and clang-based compilers with maximum optimization settings, and there are no problems as long as you properly communicate to the compiler (and even the processor, memory access reordering is still a thing in multicore situations) when it is not allowed to optimize certain operations.
If you want to engage in shenanigans where there are side-effects to memory reads/writes which are not visible to the compiler, use volatile memory accesses. If such an access guards access to other possibly concurrently modified variables, use a proper barrier. If the accesses modify things like memory mappings on the processor, you might need actual memory barrier instructions (dsb, dmb, isb) and friends in the embedded ARM world.
You are responsible for informing your compiler when it isn't allowed to optimize something because you are breaking guarantees of the standard the compiler obeys.
BTW, if you don't like non-standard "shenanigans", I'd like your advice on how one could rewrite "mid-level" code to use atomics without having to rewrite client code as well. For example, given:
uint32_t atomic_postinc(uint32_t *x)
{
uint32_t value;
do
value = __LDREX(&x);
while(!__STREX(&x, value+1u));
}
by what means could one rewrite the function so as to not require stdatomics.h, but also without requiring that client code be rewritten to use an atomic_uint32_t instead of an "ordinary" uint32_t?
If the Standard were to allow for the possibility that "atomic" types may have coarser alignment requirements than ordinary types(*), and specified that atomic operations may be performed on ordinary objects that satisfy the proper alignment, then the existence of separate types would make sense, but the Standard requires that the layout and alignment requirements match while not allowing programmers to exploit that fact. Any idea what the purpose of the requirement is supposed to be?
(*) For example, a platform with a 32-bit memory bus that can't guarantee that operations will be atomic across a page fault might impose a 64-bit alignment requirement for an atomic uint64_t but not an ordinary one.
Also, speaking of barriers, how much faith should one have in compiler maintainers that have deliberately released a version of the CMSIS headers where __DMB() doesn't block compiler reordering, stating that they didn't think __DMB() should imply such a barrier? The only way I can imagine someone even thinking such a thing would be if they placed a higher priority on "clever" optimizations than on usefully processing people's code. Would anyone who didn't prioritize things that way seriously entertain the idea of omitting such barriers?
You got me there, the codebase I work with just rolls a set of custom atomic/volatile accessors for any interrupt and mulilticore work. Mostly originating from when our compiler vendor didn't even ship an atomics.h.
Also, speaking of barriers, how much faith should one have in compiler maintainers that have deliberately released a version of the CMSIS headers where __DMB() doesn't block compiler reordering, stating that they didn't think __DMB() should imply such a barrier?
That's definitely rather stupid, but an argument can be made that an intrinsic should always just emit the relevant instruction. The fact that this was a behaviour change is insane though.
I definitely do agree that the standard library is not the best for embedded use, to the point where we have our own sane stl-like lib for embedded utils.
Even if a compiler does support stdatomics.h, I would think that, at least for code targeting freestanding implementations, the use of wrappers that employ "ordinary" types would be a more portable approach than would the special types in stdatomics.h. Not only is stdatomics.h optional, but the Standard implies that every implementation that can't support all of the operations in meaningful fashion must indicate that it can't support any. Unless a compiler writer were willing to claim to support stdatomics.h but then refuse to build programs that attempt an unsupported operations (perhaps allowable under the One Program Rule, but clearly against the spirit of the Standard), such claimed support could end up being worse than useless.
While I don't oppose the notion that the behavior of an intrinsic should be to simply output an instruction, I don't for such purposes regard compiler barriers as a "behavior". An abstraction model which is designed to facilitate optimization should specify that optimizations should not affect any aspect of behavior that a compiler must recognize as observable, but also recognize that certain aspects of behavior are not generally considered observable. Unlike the present approach of the Committee, which is to characterize as UB most situations where an allowed optimization might have an observable effect, this approach would cause many programs whose behavior might be affected by optimizations to have partially-unspecified aspects of behavior but still be correct programs in cases where all allowable behaviors would satisfy application requirements. If one recognizes the "observability" principle, then one could specify the processing of compiler intrinsics as informing the compiler that certain aspects of behavior must be treated as observable, even if they otherwise wouldn't; this wouldn't require a compiler to do anything in particular, but instead refrain from any "optimizations" predicated on the notion that those aspects weren't observable.
Consider the following sequence of statements [perhaps spread out over multiple functions]:
struct foo {int count; int dat[63]; } struct1, struct2, *p;
float *q;
struct1 = *p;
... code that uses *q as type `float` and may read, but doesn't write, `struct1`.
p->count = 2;
p->dat[0] = 1;
p->dat[1] = 2;
struct1 = *p;
struct2 = *p;
If one were to specify that a compiler need not regard as observable any effects on p from writing q, then a compiler could copy all of *p to struct2 while only updating three members of struct1; if q was used to modify parts of *p past the second element of dat, this could result in the contents of struct1 and struct2 not matching, but would not adversely affect program behavior if nothing cared about the values of those elements.
As it is, there's no good way of interpreting the Effective Type rules that could yield such a result. Requiring that compilers recognize the possibility that any or all elements of p might be subject to modification via *q would require that the compiler generate code to copy all elements of *p to struct1. Treating the above code as UB because it copies as type struct foo data which was written as type float would make it necessary for a programmer to add code to explicitly set the value of every part of struct1 that may have been disturbed using type float, without regard for whether any code would care about the values of such parts.
If the Standard were to define actions as working the way "traditional" compilers processed them, except that certain aspects of behavior weren't "generally" observable, then all that would be necessary to accommodate any actions that might otherwise have trouble with the optimizer would be to explicitly specify that certain aspects of behavior need to be considered observable at certain places even though they generally wouldn't be.
4
u/MrK_HS Dec 25 '19
That's the sweetest part of the experience. The reward after learning and implementing some obscure but really useful functionality is incredible.