Padding aligns structure members to "natural" address boundaries - say, int members would have offsets, which are mod(4) == 0 on 32-bit platform. Padding is on by default. It inserts the following "gaps" into your first structure:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserted by compiler: for alignment of b */
    int b;
    char c;
    char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;

Packing, on the other hand prevents compiler from doing padding - this has to be explicitly requested - under GCC it's __attribute__((__packed__)), so the following:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

would produce structure of size 6 on a 32-bit architecture.

A note though - unaligned memory access is slower on architectures that allow it (like x86 and amd64), and is explicitly prohibited on strict alignment architectures like SPARC.

Answer from Nikolai Fetissov on Stack Overflow
🌐
GNU
gnu.org › software › c-intro-and-ref › manual › html_node › Packed-Structures.html
Packed Structures (GNU C Language Manual)
In GNU C you can force a structure to be laid out with no gaps by adding __attribute__((packed)) after struct (or at the end of the structure type declaration).
Top answer
1 of 11
404

Padding aligns structure members to "natural" address boundaries - say, int members would have offsets, which are mod(4) == 0 on 32-bit platform. Padding is on by default. It inserts the following "gaps" into your first structure:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserted by compiler: for alignment of b */
    int b;
    char c;
    char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;

Packing, on the other hand prevents compiler from doing padding - this has to be explicitly requested - under GCC it's __attribute__((__packed__)), so the following:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

would produce structure of size 6 on a 32-bit architecture.

A note though - unaligned memory access is slower on architectures that allow it (like x86 and amd64), and is explicitly prohibited on strict alignment architectures like SPARC.

2 of 11
165

(The above answers explained the reason quite clearly, but seems not totally clear about the size of padding, so, I will add an answer according to what I learned from The Lost Art of Structure Packing, it has evolved to not limit to C, but also applicable to Go, Rust.)


Memory align (for struct)

Rules:

  • Before each individual member, there will be padding so that to make it start at an address that is divisible by its alignment requirement.
    E.g., on many systems, an int should start at an address divisible by 4 and a short by 2.
  • char and char[] are special, could be any memory address, so they don't need padding before them.
  • For struct, other than the alignment need for each individual member, the size of whole struct itself will be aligned to a size divisible by strictest alignment requirement of any of its members, by padding at end.
    E.g., on many systems, if struct's largest member is int then by divisible by 4, if short then by 2.

Order of member:

  • The order of member might affect actual size of struct, so take that in mind. E.g., the stu_c and stu_d from example below have the same members, but in different order, and result in different size for the 2 structs.

Address in memory (for struct)

Empty space:

  • Empty space between 2 structs could be used by non-struct variables that could fit in.
    e.g in test_struct_address() below, the variable x resides between adjacent struct g and h.
    No matter whether x is declared, h's address won't change, x just reused the empty space that g wasted.
    Similar case for y.

Example

(for 64 bit system)

memory_align.c:

/**
 * Memory align & padding - for struct.
 * compile: gcc memory_align.c
 * execute: ./a.out
 */ 
#include <stdio.h>

// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
    int i;
    char c;
};

// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
    long l;
    char c;
};

// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
    int i;
    long l;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
    long l;
    int i;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
    double d;
    int i;
    char c;
};

// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
    int i;
    double d;
    char c;
};

// size is 4,
struct stu_g {
    int i;
};

// size is 8,
struct stu_h {
    long l;
};

// test - padding within a single struct,
int test_struct_padding() {
    printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
    printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
    printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
    printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
    printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));

    return 0;
}

// test - address of struct,
int test_struct_address() {
    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    struct stu_g g;
    struct stu_h h;
    struct stu_f f1;
    struct stu_f f2;
    int x = 1;
    long y = 1;

    printf("address of %s: %p\n", "g", &g);
    printf("address of %s: %p\n", "h", &h);
    printf("address of %s: %p\n", "f1", &f1);
    printf("address of %s: %p\n", "f2", &f2);
    printf("address of %s: %p\n", "x", &x);
    printf("address of %s: %p\n", "y", &y);

    // g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));

    // h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));

    // f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));

    // x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
    printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
    printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));

    // y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
    printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
    printf("space between %s and %s: %ld\n", "h", "y", (long)(&y) - (long)(&h));

    return 0;
}

int main(int argc, char * argv[]) {
    test_struct_padding();
    // test_struct_address();

    return 0;
}

Execution result - test_struct_padding():

stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8

Execution result - test_struct_address():

stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0  // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0  // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc  // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8  // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between h and y: 8

Thus address start for each variable is g:d0 x:dc h:e0 y:e8

Discussions

data structures - What are the implications of a 'packed' keyword/feature? - Programming Language Design and Implementation Stack Exchange
The effect of packing is, very simply, to remove all padding.1 · 1 I am aware that some compilers take the opportunity to also allow specifying the struct alignment when packing. More on langdev.stackexchange.com
🌐 langdev.stackexchange.com
May 22, 2023
Is a packed struct always the same in size?
They can be an extremely fast and efficient way to serialize/deserialize data - but they have a lot of potential pitfalls. Once you’re working with something that is pipelined or using caches I would avoid them entirely. They are a tool for optimization and as such you can eventually suffer from a range of premature optimization problems if you don’t carefully consider what you’re attempting to do with them. The first thing is that you should only use fixed width elements. Stick to C99 integer types only. Bitfields can be very powerful but have their own set of rules you’ll need to follow. Never use a pointer type in a packed struct. As long as you stick to simple rules like that you can be assured that the struct/union won’t change sizes on you. Whatever you’re communicating with will also have to be the same endian as your device as well otherwise you’ll still need to convert them. If strings are involved at all then you’re not going to gain much. If you open up your processor headers you’ll find that the register definitions are almost always the register address cast to a struct pointer. More on reddit.com
🌐 r/embedded
50
30
November 22, 2022
The Lost Art of Structure Packing (2018)
I was recently doing that in the context of improving a language’s library support for something that had to go through the OS SDK. I don’t miss the days when that stuff was the norm · struct Obj { int foo; bool bar; } then we were storing those in a custom hashmap using these as keys, ... More on news.ycombinator.com
🌐 news.ycombinator.com
112
112
April 29, 2020
What's the equivalent of C struct{...} __attribute__((packed))?
I am struggling to find a way in zig to represent packed struct in C, tried packed struct and extern struct, both don’t seem to fit, i.e. the size of both are not exactly the sum of all fields in it. any hints would be appreciated. More on ziggit.dev
🌐 ziggit.dev
1
0
March 31, 2025
🌐
Reddit
reddit.com › r/embedded › __attribute__((packed)) in embedded systems, pros and cons?
r/embedded on Reddit: __attribute__((packed)) in embedded systems, pros and cons?
April 8, 2024 -

I am working on this C project and I have noticed that attribute((packed)) is used quite alot. I get that it helps optimise memory usage on the detriment of cpu performance. Is it a common practice to favor memory over performance in embedded projects? Or am I missing/misunderstanding something?

🌐
Python
docs.python.org › 3 › library › struct.html
struct — Interpret bytes as packed binary data
February 23, 2026 - Return a bytes object containing the values v1, v2, … packed according to the format string format. The arguments must match the values required by the format exactly. struct.pack_into(format, buffer, offset, v1, v2, ...)¶
🌐
Zig Guide
zig.guide › packed structs
Packed Structs | zig.guide
January 4, 2026 - Sometimes you may want to have struct fields with a defined layout that do not conform to your C ABI. packed structs allow you to have extremely precise control of your struct fields, allowing you to place your fields on a bit-by-bit basis.
Top answer
1 of 7
10

Padding and Packing

The layout of structures is defined by the ABI (Application Binary Interface). The typical mindset of an ABI designer is mechanical sympathy, with an eye towards performance (speed).

To talk about padding, we need to talk about alignment, size, and stride.

First is alignment. Native types tend to have an alignment, that is the architecture expects that a double is either 4 or 8 bytes aligned (depending). This alignment then bleeds into compound types: the only way to ensure that a double field is 8 bytes aligned is for the compound type containing the field to itself by 8 bytes aligned, hence a compound type's alignment is the greater alignment of any of its components.

Second is size. Native types have a size, too. For example a double is typically 8 bytes. The size of the compound type will depend on the size and alignment of its components, as per the ABI rules.

Finally is stride. The stride of a type is the spacing between consecutive array elements. Swift is one of the few languages differentiating size and stride, in C the stride is always the size.

With that in mind, here is a typical product type ABI, as can generally be found in C:

  • The alignment of the product type is the maximum alignment of any of its fields.
  • The first field starts at the first byte of its product type.
  • Any subsequent field starts at the next offset which guarantees its alignment, which may lead to "in-between" padding bytes.
  • The size of the struct is rounded-up to a multiple of its alignment, which may lead to "tail" padding bytes.

And since a picture is clearer than words:

struct ProductType {
    int i;
    char __padding_0[P0];
    double d;
    char __padding_1[P1];
    char c;
    char __padding_2[P2];
};

On x64, we have:

  • int: 4 bytes, 4-bytes aligned.
  • double: 8 bytes, 8-bytes aligned.
  • char: 1 byte, 1-byte aligned.

Therefore:

  • P0 = 4. This is because d must start at an offset that is a multiple of 8, and after i the offset is only 4.
  • P1 = 0. This is because c must start at an offset that is a multiple of 1, and any offset is.
  • P2 = 7. This is because the alignment of ProductType is 8, hence its size must be a multiple of 8.

Note that Swift would have P2 = 0.

The effect of packing is, very simply, to remove all padding.1

1 I am aware that some compilers take the opportunity to also allow specifying the struct alignment when packing. That's a potentially useful extra feature, but it's not packing.

Implications of Packing

The first and foremost implication is that you should implement it properly, or users will curse you.

For example, it seems reasonable to allow taking a reference (or pointer) to a field of a packed struct. However, this reference (or pointer) may now be under-aligned: that is, its alignment may be strictly less than the expected alignment of a pointer of this type.

Different languages & toolchains handle the situation differently:

  • In Zig, pointer alignment is a part of the pointer type, so a compilation error will follow if one attempts to use an under-aligned pointer where a regular pointer is expected.
  • In GCC (C or C++), the compiler handles under-aligned pointers properly within the function in which the pointer was created, but allows passing the under-aligned pointers to functions expecting a regular pointer... which leads to Undefined Behavior.
  • In Rust, manipulating packed fields is unsafe, and the user is on the hook to tread carefully, or they will trigger Undefined Behavior.

I would argue that the situation of being able to create an under-aligned pointer, but then have to walk on eggshells forever, is not desirable from a user point of view, and thus that language designers going the packed way should either disallow forming such pointers, or follow through and ensure they provide users the tooling they need to manipulate under-aligned pointers.

Alternatives

There are alternatives to packing, for the memory conscious.

As mentioned, packing is about avoiding padding, and there are two sources of padding:

  • In-between fields padding.
  • Tail padding.

Tail padding is eliminated in a language such as Swift which differentiates size & stride.

In-between fields padding is eliminated in a language such as Rust which reserves the right to re-order fields arbitrarily -- and typically will reorder them by descending alignment, which removes all in-between fields padding.

Those two alternatives, combined, remove nearly2 any and all padding in a struct3 without introducing under-aligned references and pointers into the fray.

I would typically advise considering them first:

  • Provide the same memory gains as packed, without taxing the user.
  • Without performance penalty incurred from under-aligned pointers.
  • Without headaches induced by under-aligned pointers.

You may still want to later introduce under-aligned pointers in the language, separately.

2 Unless reordering can interleave the fields from different field structs, there may still be some padding in between structs. For example, a struct A (int, char) followed by itself would still have 3 bytes of padding between the two instances, no matter the order.

3 Padding may still occur between array elements, the exact amount of padding inserted being equal to stride - size.

2 of 7
9

According to “The Lost Art of Structure Packing” by Eric S. Raymond, structure packing can slow down your program or worse:

The first thing to understand is that, on modern processors, the way your compiler lays out basic datatypes in memory is constrained in order to make memory accesses faster. Our examples are in C, but any compiled language generates code under the same constraints.

Self-alignment [that is, aligning to an address divisible by the size of the datatype] makes access faster because it facilitates generating single-instruction fetches and puts of the typed data. Without alignment constraints, on the other hand, the code might end up having to do two or more accesses spanning machine-word boundaries.

I said “on modern processors” because on some older ones forcing your C program to violate alignment rules (say, by casting an odd address into an int pointer and trying to use it) didn’t just slow your code down, it caused an illegal instruction fault.

On the other hand, structure packing is useful “if you intend to write code for memory-constrained embedded systems, or operating-system kernels. It is useful if you are working with application data sets so large that your programs routinely hit memory limits. It is good to know in any application where you really, really care about optimizing your use of memory bandwidth and minimizing cache-line misses.” Raymond goes on to describe a program of his that kept getting OOM errors, for which he managed “to cut the working-set size by around 40%” by carefully arranging his structures.

Find elsewhere
🌐
Rust
docs.rs › packed_struct
packed_struct - Rust
Infos about a particular type that can be packaged. ... A structure that can be packed and unpacked from a slice of bytes.
🌐
GeeksforGeeks
geeksforgeeks.org › c language › how-to-pack-a-struct-in-c
How to Pack a Struct in C? - GeeksforGeeks
July 23, 2025 - In C, when you declare a structure, ... alignment purposes. struct packing refers to the arrangement of the members of a structure in memory so that there is no extra space left....
🌐
Reddit
reddit.com › r/embedded › is a packed struct always the same in size?
r/embedded on Reddit: Is a packed struct always the same in size?
November 22, 2022 -

Can a struct with packed attribute be always the same in size?

How can I be sure that every element will be the same?

I want to avoid parsing a buffer and just cast the buffer and assign it into a pointer. But I have to be sure that the struct will remain the same in size across compilations

PS: I mean the same without removing or adding members

Top answer
1 of 11
26
Embedded dev of 20+ years here. My advice would be to avoid packed structs at all, if possible. Packed structs have a number of severe disadvantages. The two most prominent ones are bad portability and the risk of getting undefined or flat out erraneous behaviour. Portability: The position of members in a struct obviously depends on the size of those members. Some data types, however, have different sizes on different CPU architectures. On x64, a "long" has a size of 64 bits / 8 bytes whereas on ARMv7 it has 32bits / 4 bytes. Alignment: Many data types require a certain memory alignment. On ARMv7, double floating point values *must* be placed on an address which is a multiple of 4. On x64, the same data type can be accessed anywhere. This code will run just nicely on x86/x64 but produce a bus error on ARMv7 because the member "aFloat" will be placed on a address which is not a multiple of 4: struct __attribute__((__packed__)) { char aFlag; double aFloat; } MyStruct; MyStruct.aFloat = 3.1415; Some developers use structs (packed or unpacked) to access hardware registers: struct MyController { unsigned long REG_CFG; unsigned long REG_DATA; } struct MyController *pHW = getControllerAddress() pHW->REG_CFG = 0x00000000; This is another recipe for disaster because nobody will guarantee, that the members align correctly with the controller's registers. The Linux kernel explicitely forbids this coding practice because it may introduce hard to find errors in drivers, TL;DR: Unless you have exactly ONE piece of HW on ONE CPU architecture: Don't use packed structs Don't use any structs when addressing hardware
2 of 11
25
Ignore all these people telling you not to use packed structs. They are essential and if you use some simple rules they are portable and not dangerous at all. Use the types defined in . In there you will find types such as uint8_t, uint16_t and uint32_t. These types are portable and guaranteed to be the same size on all platforms that support them. Use only these numeric types or other packed struct types in your packed structs. Use compile-time asserts and the sizeof operator to ensure that the size of your struct is what you expect it to be. Use compile-time asserts and offsetof to check individual fields if you are paranoid about their positions. The only danger with portability here is not struct layout but endianness. If you serialize a struct on a big-endian machine and then try to read it on a little-endian machine then all your 2- and 4-byte fields will need to be byte-swapped. For myself, I use C++ and define class types called “le_uint16_t” and “be_uint16_t” (and the same for 32-bit types) and these will do appropriate swapping depending on the endianness of the build target.
🌐
Joshcaratelli
joshcaratelli.com › blog › struct-packing
The forgotten art of Struct Packing in C/C++. - Code ...
January 15, 2018 - The easiest way to pack a struct is to order them from largest to smallest, as a struct is always aligned to their largest data type. E.g. this structs largest member is a pointer (8 bytes on 64 bit architecture).
🌐
NVIDIA
docs.nvidia.com › cuda › parallel-thread-execution
1. Introduction — PTX ISA 9.2 documentation
Packed fixed-point data type cannot be used as fundamental type and is only supported as instruction type. PTX includes built-in opaque types for defining texture, sampler, and surface descriptor variables. These types have named fields similar to structures, but all information about layout, ...
🌐
Hacker News
news.ycombinator.com › item
The Lost Art of Structure Packing (2018) | Hacker News
April 29, 2020 - I was recently doing that in the context of improving a language’s library support for something that had to go through the OS SDK. I don’t miss the days when that stuff was the norm · struct Obj { int foo; bool bar; } then we were storing those in a custom hashmap using these as keys, ...
🌐
Catb
catb.org › esr › structure-packing
The Lost Art of Structure Packing
The restriction that bitfields cannot cross machine word boundaries means that, while the first two of the following structures pack into one and two 32-bit words as you’d expect, the third (struct foo9) takes up three 32-bit words in C99, in the last of which only one bit is used.
🌐
Microsoft Learn
learn.microsoft.com › en-us › answers › questions › 1140997 › c-struct-packing-memory-consumption
C++ struct packing memory consumption - Microsoft Q&A
December 23, 2022 - Or is there a way for the compiler to not necessarily follow the struct's specified layout, but instead reorder the member variables in whatever way reduces memory consumption the most? I figure that there's probably a compiler flag to do this. ... A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation. ... #pragma pack(push, 1) struct examplestruct { unsigned char varb; unsigned int varc; unsigned char vacd; unsigned char vare; }; #pragma pack(pop)
Top answer
1 of 5
9

When programming in C I have found it invaluable to pack structs using GCCs __attribute__((__packed__)) [...]

Since you mention __attribute__((__packed__)), I assume your intention is to eliminate all padding within a struct (make each member have a 1-byte alignment).

Is there no standard for packing structs that works in all C compilers?

... and the answer is "no". Padding and data alignment relative to a struct (and contiguous arrays of structs in stack or heap) exist for an important reason. On many machines, unaligned memory access can lead to a potentially significant performance penalty (though becoming less on some newer hardware). In some rare-case scenarios, misaligned memory access leads to a bus error that is unrecoverable (may even crash the entire operating system).

Since the C standard is focused on portability, it makes little sense to have a standard way to eliminate all padding in a structure and just allow arbitrary fields to be misaligned, since to do so would potentially risk making C code non-portable.

The safest and most portable way to output such data to an external source in a way that eliminates all padding is to serialize to/from byte streams instead of just trying to send over the raw memory contents of your structs. That also prevents your program from suffering performance penalties outside of this serialization context, and will also allow you to freely add new fields to a struct without throwing off and glitching the entire software. It'll also give you some room to tackle endianness and things like that if that ever becomes a concern.

There is one way to eliminate all padding without reaching for compiler-specific directives, though it's only applicable if the relative order between fields does not matter. Given something like this:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... we need the padding for aligned memory access relative to the address of the structure containing these fields, like so:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... where . indicates padding. Every x must align to an 8-byte boundary for performance (and sometimes even correct behavior).

You can eliminate the padding in a portable way by using an SoA (structure of array) representation like so (let's assume we need 8 Foo instances):

struct Foos
{
   double x[8];
   char y[8];
};

We've effectively demolished the structure. In this case, the memory representation becomes like this:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... and this:

01234567
yyyyyyyy

... no more padding overhead, and without involving misaligned memory access since we're no longer accessing these data fields as an offset of a structure address, but instead as an offset of a base address for what is effectively an array.

This also carries the bonus of being faster for sequential access as a result of both less data to consume (no more irrelevant padding in the mix to slow down the machine's relevant data consumption rate) and also a potential for the compiler to vectorize processing very trivially.

The downside is that it's a PITA to code. It's also potentially less efficient for random access with the larger stride in between fields, where often AoS or AoSoA reps will do better. But that's one standard way to eliminate padding and pack things as tightly as possible without screwing with the alignment of everything.

2 of 5
12

In a struct, what matters is the offset of each member from the address of each struct instance. Not so much is the matter of how tightly things are packed.

An array, however, matters in how it is "packed". The rule in C is that each array element is exactly N bytes from the previous, where N is the number of bytes used to store that type.

But with a struct, there is no such need for uniformity.

Here's one example of a weird packing scheme:

Freescale (who make automotive microcontrollers) make a micro that has a Time Processing Unit co-processor (google for eTPU or TPU). It has two native data sizes, 8 bits and 24 bits, and only deals with integers.

This struct:

struct a
{
  U24 elementA;
  U24 elementB;
};

will see each U24 stored its own 32 bit block, but only in the highest address area.

This:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

will have two U24s stored in adjacent 32 bit blocks, and the U8 will be stored in the "hole" in front of the first U24, elementA.

But you can tell the compiler to pack everything into its own 32 bit block, if you want; it's more expensive on RAM but uses less instructions for accesses.

"packing" doesn't mean "pack tightly" - it just means some scheme for arranging elements of a struct wrt the offset.

There is no generic scheme, it is compiler+architecture dependent.

🌐
NXP
community.nxp.com › t5 › LPCXpresso-IDE-FAQs › Packed-Structures › td-p › 473585
Packed Structures - NXP Community
March 31, 2016 - By default, GCC does not pack structures so it will in general leave spaces so that data items are word aligned. This behavior can be changed by using the "packed" attribute on the structure. struct foo { short a; char b; int c; char d; } __attribute__((packed)); Some other toolchains ha...
🌐
Post.Byes
post.bytes.com › home › forum › topic
Packed structs vs. unpacked structs: what's the difference? - Post.Byes
April 9, 2006 - The packed keyword, to the best of my knowledge, overrides compiler optimisations which lead to natural boundary alignment of one or more of the data elements contained in the structure. A "packed" structure will almost always have a smaller memory footprint than its unpacked brother.