I'm not sure I understand what you mean. I don't see testing as separate from development, and the way I write code, I don't require specific machine code to be generated, only that it performs as expected. I don't mind if a new compiler version might optimize the code differently.
No matter how hard you try, there may be bugs in your code, which may or may not manifest themselves depending on compiler settings and other circumstances. Therefore, before you ship anything in binary form, it should be tested.
Obviously; so obviously in fact, that I consider that testing to be a natural part of the development. It is not something done "after the project is complete", but at all phases of the project, too.
For example, seeing how users actually use the software, I typically realize new types of tests that can verify the system works as designed. I expect this, because no specification is truly complete (or even
correct) up front, no matter how well written, and consider this too part of the development process.
In practice, I look at development activity as a curve rather similar to typical product lifetimes. There is a big hump up front, then it tapers down slowly. Adding new features, new versions, refactoring, adds their own humps, and rewriting yields very much a new curve.
It is because of this, because of how closely intertwined testing should be with development –– they should feed each other, continuously –– that I don't understand the idea of freezing the code base for a while. I understand freezing in the sense of not adding new features, and working on fixing bugs found in testing, but not leaving the code as is during testing.
The nice thing is that the compiler is allowed to optimize the inside of a critical section however it likes, as long as memory is consistent at the end. It can also optimize by moving accesses into the critical section, but it can't move them out.
Another option is compiler memory barrier, using
asm volatile (""::"r"(buf):"memory"), where
buf is the buffer modified. It works with both GCC and Clang; the compiler will not move any memory accesses to
buf over such a barrier, nor eliminate stores to it, before the barrier. (See related
llvm bug (#15495) discussion.) It is used in the Linux kernel, via the
barrier_data macro, for example to ensure that when calling
memzero_explicit(s,n), the buffer is truly cleared via
memset(s,0,n); and not eliminated because it is not accessed afterwards.
I do suspect that such a compiler memory barrier would have worked for peter-h's use cases, but I'd need to know the precise details of the use case pattern to be sure. If the data involved is all in SRAM that does not get modified by hardware (other than possibly DMA initiated after the barrier), then it would suffice.
Of course, while it conforms to the C11 memory model, it is
not standard C at all, so whether it is a suitable solution worth considering depends on the situation. Assuming the target is a 32-bit ARM Cortex-M, then I would personally use a compiler memory barrier instead of volatile, when clearing or copying firmware-related stuff from the SRAM.