Optimizing C Structures: The Hidden Cost of Memory Padding
A C struct holding 13 bytes of data can quietly occupy 24. Here is how to measure padding with sizeof and offsetof, and reclaim the waste by reordering members.
31 Aug 2026, 13:14 UTC

A C structure holding 13 bytes of real data can quietly occupy 24 bytes of memory. The extra bytes are padding: invisible filler the compiler inserts so each member sits at an address the CPU can access efficiently. In an array of a million such structures, that hidden overhead becomes real, measurable waste. The fix usually costs nothing more exotic than sorting member declarations from largest to smallest and confirming the result with sizeof and offsetof.
Why the Compiler Inserts Padding
CPUs fetch memory in aligned chunks rather than byte by byte. When a member starts at an address that is not a multiple of its alignment requirement—typically its own size for scalar types—the hardware may need extra accesses, and on some architectures it may refuse the access outright. Compilers therefore place each member at a suitable offset, inserting padding bytes wherever the previous member ended short.
Two rules follow. Every member gets an offset that is a multiple of its alignment requirement, and the total struct size is rounded up to a multiple of the largest member's alignment so that every element in an array of the struct stays aligned. That second rule is why structs can carry padding at the end, not only between members.
The exact rules come from the target's ABI (Application Binary Interface) and are implementation-defined. The numbers below assume a common 64-bit ABI such as x86-64 System V; always re-check on your own target.
Worked Example: 24 Bytes Down to 16
Both structures below store identical data. Only the declaration order differs:
#include <stdio.h>
#include <stddef.h>
struct sensor_naive {
char id; /* 1 byte, then 7 bytes of padding */
double value; /* 8 bytes */
int status; /* 4 bytes, then 4 bytes of tail padding */
};
struct sensor_sorted {
double value; /* 8 bytes */
int status; /* 4 bytes */
char id; /* 1 byte, then 3 bytes of tail padding */
};
int main(void)
{
printf("naive: %zu bytes\n", sizeof(struct sensor_naive));
printf("sorted: %zu bytes\n", sizeof(struct sensor_sorted));
return 0;
}
Compile and run it on your development machine; no special permissions are needed:
gcc -Wall -Wextra -o layout layout.c
./layout
Under the x86-64 System V ABI the expected results are 24 bytes for sensor_naive and 16 bytes for sensor_sorted. Both structs hold 13 bytes of members; the naive order wastes 11 bytes on padding, the sorted order only 3. Because padding is implementation-defined, treat your program's printed numbers, not this article, as the source of truth for your target.
| Layout | Member | Offset | Size | Padding |
|---|---|---|---|---|
| naive | id | 0 | 1 | 7 |
| naive | value | 8 | 8 | 0 |
| naive | status | 16 | 4 | 4 (tail) |
| sorted | value | 0 | 8 | 0 |
| sorted | status | 8 | 4 | 0 |
| sorted | id | 12 | 1 | 3 (tail) |
Scaled to an array of one million readings, the reorder saves roughly 8 MB of RAM without touching a line of logic.
Pinpointing the Gaps with offsetof()
offsetof(type, member) from <stddef.h> reports a member's byte offset from the start of the struct, which makes it the quickest way to see where the compiler hid the filler:
printf("id at %zu\n", offsetof(struct sensor_naive, id));
printf("value at %zu\n", offsetof(struct sensor_naive, value));
printf("status at %zu\n", offsetof(struct sensor_naive, status));
If a member's offset exceeds the combined size of everything declared before it, the difference is padding. Clang can also flag padding at compile time, which is handy during review:
clang -Wall -Wextra -Wpadded -c layout.c
-Wpadded is a Clang warning; GCC has no direct equivalent, so the sizeof and offsetof check remains the portable verification method.
When Reordering Is Off the Table
Reordering changes a struct's binary layout. If the layout is dictated from outside—a file format, a network protocol, a hardware register map, or memory-mapped device registers—leave the order alone. The same caution applies to code that writes structs wholesale with fwrite or compares them with memcmp: both bake the padded layout into data that breaks the moment the struct or the compiler changes. Serialize field by field instead.
The packed shortcut and its price
Compiler extensions such as __attribute__((packed)) (GCC and Clang) or #pragma pack (MSVC) strip padding entirely. Reach for them only when an externally imposed layout leaves no alternative, and know the costs:
- On architectures that require aligned access—some ARM and SPARC targets among them—dereferencing a pointer to a misaligned member can fault.
- Even where unaligned access works, such as x86, it is typically slower than aligned access.
- Atomic operations on misaligned members are not reliably supported.
When you must read a packed field, copy it into a properly aligned local with memcpy rather than dereferencing a pointer into the packed struct.
A Closing Checklist
- Declare members from largest to smallest alignment requirement; for scalars that generally means sorting by size.
- Verify with
sizeof, and investigate surprises withoffsetof. - Keep Clang's
-Wpaddedin your review build to catch new padding as structs evolve. - Reserve
packedfor externally fixed layouts, and reach packed members throughmemcpy. - Re-check the numbers whenever the target architecture or compiler changes, because padding rules follow the ABI.
Padding is not a bug; it is the compiler keeping your data fast to access. But when a one-line reorder removes a third of a hot structure's footprint, the minute it takes to check is well spent.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.