In our last project I presented the library for BLE P Click board. If you were specially curious, and looked inside the source files, you would see structures with something extra in their declaration: " _aci_packed_ ". _aci_packed was actually a macro which was declared as: __attribute__((__packed__)). This command tells the compiler to pack structures. What are packed structures and why do we use them? We will explore this topic today. First we will have to go over the terms of alignment and padding.
Alignment and Padding
Let's say that we have a 32-bit architecture, and we created a struct like the one below:
struct my_struct { char a; int b; };
This structure is automatically aligned and padded. Since we have a 32-bit (4 byte) system, integer values take up 4 bytes of memory. What the compiler does is, it stores them on address space which is divisible by 4 (4 mod 0 == 0). Similar thing goes for 2-byte variables also. By doing so, the CPU can access these members faster than when not aligned. So what happens in memory when you declare a 1-byte variable and then a 4-byte one, like we did above? Since we have a 32-bit architecture, our memory is divided in registers of 4 bytes (32 bits). One of those 4 bytes is occupied by the char. The integer value is not going to be stored next, rather, the compiler will add padding for the next 3 bytes, and then add the integer at the next address. So what our structure actually looks like is like this:
struct my_struct { char a; char padding[3]; int b; };
This way, our struct is aligned, and the int value is stored in an easily accessible space for the CPU.
The disadvantage, however, is that this makes our struct 3 bytes bigger than what we actually use. If we would count the bytes, we will see that: (char a) 1 byte + (Padding) 3 bytes + (int b) 4 bytes = 8 bytes are used by memory, while we in our application only use 5 bytes ( one char and one int). If we would call
sizeof(my_struct)
We would actually get 8 returned.
Now let's look at pointer arithmetic, if we would take a one-byte pointer:
uint8_t *my_ptr = my_struct;
Pointing to our struct, it would currently point at our char a. If we incremented our pointer:
my_ptr++;
It would not point to our integer, but to padding bytes. This would significantly mess up the outcome of our code. For example, we would send wrong data from our struct to our slave device, thus get unexpected behavior.
Packing the structures
Now, if we declare our struct this way:
struct __attribute__((__packed__)) my_struct { char a; int b; };
The padding would be suppressed. The next byte after the char byte would be the first byte of our integer. And when we increment our pointer, it would actually point to the integer. The advantage is obvious - we don't waste memory space, our sizeof(struct) will return the expected value, and we could walk through our struct with our pointer without worrying about getting wrong data. The disadvantage, however, is that our memory access would be slower. So if you depend on real time results, watch out for this.
Other workarounds
Manual packing
There are, however, ways to pack our structures "manually". That is, if we have more members. Let's say our structure looks like this:
struct my_struct { char a; int b; char c; char d; int e; char f; };
We would have padding everywhere between a char and an integer. Let's take a look what our memory actually looks like:
struct my_struct { char a; char padding_a[3]; int b; char c; char d; char padding_d[2] int e; char f; };
We would have additional 5 bytes. But if we re arrange our struct a little bit:
struct my_struct { char a; char c; char d; char f; int b; int e; };
We would have first 4 bytes taken up with our char variables, which is one word of memory. So when we declare int b, no padding will be required. Everything fits perfectly. This way we manually packed our struct for better memory usage.
Bit fields
If you don't want to re arrange the members of your struct, and you neither want to use the packed attribute, there's still one more solution - bit fields.
Let's say we have some variables in our struct, and that those variables can hold only values 0 and 1:
struct my_struct { uint8_t a; uint8_t b; };
This would work, but it would take up 2 bytes. With bit fields, we can save memory significantly:
struct my_struct { uint8_t a : 1; uint8_t b : 1; };
Since bits can represent 0 and 1, we can use them to store the values for our struct. When declared this way, we sat that "a" and "b" occupy exactly one bit, so the used memory space would be 1 byte instead of 2.
Add and Subtract
When talking about embedded development, memory is quite a hot topic. Different MCUs have different storage room, so when we are developing big projects, memory tends be in short supply. That is probably the main reason for why we use these methods to optimize our space. Whether you will pack your structures with the attribute, manually, or use bit fields, managing your memory is always a good habit which will keep you away from serious trouble.
References
"C -Bit Fields" tutorialspoint.com 2016
"Data Structure Alignment"" wikipedia 2016
Eric S. Raymond "The Lost Art of C Structure Packing" 2013