Optimized MCU Code and Strict Aliasing: Replace Unsafe Type-Punning at Binary Boundaries
When a binary conversion works without optimization and changes under an optimized build, inspect the object-access types before blaming the compiler. Casting a pointer does not grant permission to read the same storage through an unrelated type. For an object-representation copy, use a defined byte-copy operation into a correctly declared destination, then separately verify size, representation and byte-order requirements.
This guide covers C11 code compiled with GCC 14.2.0. The executed probes ran on an x86-64 host, not an MCU. They demonstrate an aliasing failure pattern and a defined replacement; they do not establish a target’s floating-point encoding, unaligned-access behavior or generated instruction sequence. C++ object-lifetime rules require their own review.
Separate numeric conversion from representation copying
A numeric cast from floating point to integer computes an integer value under the language’s conversion rules. Copying a floating object’s bytes into an unsigned integer preserves a representation instead. These operations answer different questions. First decide whether the application needs a numeric quantity, local representation bits or a protocol-defined byte sequence.
The public WG14 N1570 C11 committee draft, section 6.5, specifies permitted access types and includes character-type access to object representations. Its section 7.24.2.1 defines memcpy() as a byte-copy operation with nonoverlapping objects. Those rules support a copy into a separately declared destination without inventing an incompatible typed pointer into the source.

Recognize the incompatible-pointer pattern
The following original function is intentionally invalid when the two arguments designate overlapping integer and floating storage. It is included only as a diagnostic counterexample, not as production code:
int bad(int *p, float *q) {
*p = 1;
*q = 0.0f;
return *p;
}
An optimizer can reason from the permitted aliasing relationships of the parameter types. The GCC 14.2.0 optimization manual documents strict-aliasing optimization and enables it at -O2, -O3 and -Os. A program that violates the underlying access rules has no reliable result to preserve.
The host fixture passed addresses of overlapping union members to a no-inline version of this function. It printed zero with -O0 and one with -O2. Those are observations of this build, not required outcomes of undefined behavior. They do not show a compiler defect or promise that another compiler will fail in the same visible way.
GCC documents specific type-punning behavior when access occurs through a union type. That does not make every pointer obtained from a union safe for unrelated typed access. Nor does adding an intermediate void * change the type of the object ultimately accessed. Review the final dereference, not only the cast syntax.
Use a declared destination for representation bits
This original replacement was compiled and executed in the fixture:
uint32_t representation(float value) {
uint32_t bits;
_Static_assert(sizeof bits == sizeof value,
"equal object sizes required");
memcpy(&bits, &value, sizeof bits);
return bits;
}
The translation unit includes stdint.h and string.h. Its separately declared uint32_t destination avoids an incompatible integer dereference into the float object. The size assertion prevents an accidental over-read or partial representation copy when the assumed sizes differ.
On the tested host, copying 1.0f produced hexadecimal 3f800000 at both optimization levels. The optimized representation function used a register move and return, with no external memcpy call. This demonstrates that the defined source form need not imply a costly library call in that build; it is not a performance guarantee for other targets.
Keep the remaining binary-boundary checks
- Representation: equal size does not prove that a target uses the floating-point format expected by a protocol. Copying arbitrary incoming bytes into a float can create an unsupported or problematic representation.
- Byte order: a local representation integer is not automatically a serialized network field. Decode and encode the protocol’s byte order explicitly.
- Alignment: a byte buffer may not be aligned for a typed pointer cast. Copying into a properly declared local object avoids requiring the source byte array to have that destination alignment.
- Extent and overlap: validate source and destination lengths. Use the appropriate operation if regions overlap; ordinary memcpy does not permit overlap.
- Lifetime and concurrency: the source must still exist and remain stable during copying. Memcpy does not make concurrent modification safe.
For structured packets, do not replace every cast with one memcpy into a native structure and declare the format solved. Padding, field layout and representation can remain incompatible. A hypothetical device receiving a compact status frame should decode fields according to the frame specification into application objects, rather than depend on the compiler’s structure layout.
Diagnose an optimization-sensitive failure systematically
- Reduce the failing path to the exact reads and writes around the binary boundary. Preserve the original types and optimization flags.
- Identify the declared or effective type of each object and every final access through a pointer.
- Check alignment, bounds and lifetime independently; several undefined behaviors can coexist.
- Replace representation reinterpretation with a defined copy or explicit field decoding, without changing the intended value semantics.
- Compare optimized behavior and inspect generated code for the corrected form. Keep test vectors for boundary values and malformed inputs.
Disabling strict-aliasing optimization can be a diagnostic experiment, but it does not repair alignment, lifetime or data races. Treat a changed symptom as evidence to investigate, not a complete fix. The executed host probes used GCC 14.2.0 with C11 and separate O0/O2 builds; no MCU code or sanitizer run is claimed.
The acceptance criterion is a defined access path whose representation assumptions are documented and tested on the target. Once that is established, optimization becomes a code-generation and performance question rather than an accidental test of whether invalid pointer reinterpretation happens to work.