Struct Padding and Packing in C/C++

Contents

Author: Greg Gutmann
Affiliation: Tokyo Institute of Technology

Introduction

Structs are often used explicitly and or implicitly, i.e. defined by the programmer or predefined in a library or language. The compiler may add padding between members to keep each one aligned, so the order of the members can change the size of a struct. In the first example below, the same members take 16 or 20 bytes depending on their order.

For one struct this hardly matters, but it adds up in large arrays. Four extra bytes per element is 4 MB for a million particles, and on a GPU that padding is copied to the device and read from memory along with the real data. The second example removes the padding completely by packing the struct. The last one uses bit fields to fit several flags in a single byte.

Implicit Structure Padding

playerA and playerB hold the same members in a different order. The _padded versions write out the padding the compiler adds.

#include <stdio.h>      // printf 

//This structure is defined already if using CUDA
struct float3 {
	float x;
	float y;
	float z;
};
//-----------------------
struct playerA {
	bool alive;
	bool afk;
	float3 location;
};
struct playerA_padded {
	bool alive;
	bool afk;
	char padding1[2];
	float3 location;
};
//-----------------------
struct playerB {
	bool alive;
	float3 location;
	bool afk;
};
struct playerB_padded {
	bool alive;
	char padding1[3];
	float3 location;
	bool afk;
	char padding2[3];
};

int main()
{
	printf("Size of bool:    %zu\n", sizeof(bool)); // sizeof returns a type of size_t, to print use %zu
	printf("Size of char:    %zu\n", sizeof(char)); // %zu: z is length specifier, u is format type
	printf("Size of float:   %zu\n\n", sizeof(float));// %d will often work here but some compiler may complain

	printf("Size of float3:  %zu\n\n", sizeof(float3));

	printf("Size of playerA: %zu\n", sizeof(playerA));
	printf("Size of playerB: %zu\n\n", sizeof(playerB));

	printf("Size of playerA_padded: %zu\n", sizeof(playerA_padded));
	printf("Size of playerB_padded: %zu\n\n", sizeof(playerB_padded));

	return 0;
}

Output (g++ on x86-64 Linux):

Size of bool:    1
Size of char:    1
Size of float:   4

Size of float3:  12

Size of playerA: 16
Size of playerB: 20

Size of playerA_padded: 16
Size of playerB_padded: 20

float3 needs 4-byte alignment, so the compiler pads after the bools. playerB also gets 3 bytes at the end, because the size of a struct is rounded up to a multiple of its alignment. Keeping the two bools together saves 4 bytes per player.

Explicit Structure Packing and Alignment

Packing removes the padding. It is mostly used for data that leaves the program, such as network packets or binary files, where every byte counts and each field must sit at the offset the format defines.

#include <stdio.h>      // printf 

struct float3 {
	float x;
	float y;
	float z;
};

// With gcc packing can be explicitly requested using the following
// nvcc accepts it too, but GPUs require natural alignment, so in device code each
// unaligned float is read one byte at a time (4 loads instead of 1)
// On CPUs it leads to unaligned memory access which is slower on architectures that allow it (like x86 and amd64)
// Therefore this is just for those that are curious or need more compact data vs more performance
struct __attribute__((__packed__)) playerA_packed {
	bool alive;
	bool afk;
	float3 location;
};
struct __attribute__((__packed__)) playerB_packed {
	bool alive;
	float3 location;
	bool afk;
};

// The following syntax can be used with CUDA code, 
//struct __align__(16) float3Aligned {
//	float x;
//	float y;
//	float z;
//};

int main()
{
	printf("Size of playerA_packed: %zu\n", sizeof(playerA_packed));
	printf("Size of playerB_packed: %zu\n", sizeof(playerB_packed));

	return 0;
}

Output:

Size of playerA_packed: 14
Size of playerB_packed: 14

Bit Fields

When members only need a few bits, bit fields go further than reordering. Each bool normally takes a whole byte; as a 1-bit field, eight of them fit in one byte.

#include <stdio.h>      // printf 

struct float3 {
	float x;
	float y;
	float z;
};

struct flags {
	bool alive;
	bool afk;
	bool muted;
	bool invisible;
	bool admin;
	bool bot;
	bool spectator;
	bool ready;
};
// The number after the colon is how many bits the member uses
struct flags_bits {
	bool alive : 1;
	bool afk : 1;
	bool muted : 1;
	bool invisible : 1;
	bool admin : 1;
	bool bot : 1;
	bool spectator : 1;
	bool ready : 1;
};
struct playerA_bits {
	bool alive : 1;
	bool afk : 1;
	float3 location;
};

int main()
{
	printf("Size of flags:        %zu\n", sizeof(flags));
	printf("Size of flags_bits:   %zu\n", sizeof(flags_bits));
	printf("Size of playerA_bits: %zu\n\n", sizeof(playerA_bits));

	flags_bits f = {}; // all bits start at 0
	f.afk = true;
	f.ready = true;
	printf("alive: %d, afk: %d, ready: %d\n", f.alive, f.afk, f.ready);

	return 0;
}

Output:

Size of flags:        8
Size of flags_bits:   1
Size of playerA_bits: 16

alive: 0, afk: 1, ready: 1

playerA_bits is still 16 bytes: the two flags now share one byte, but float3 still needs 4-byte alignment, so the padding stays. Bit fields help most when many small fields sit together.

The compiler decides how bit fields are laid out, including the order of the bits, so they are not a reliable way to match a network packet or file format; portable code uses shifts and masks there instead. A bit field also has no address (&f.afk does not compile), and reading or writing one usually takes extra shift and mask instructions.

Conclusion

Ordering members from largest to smallest alignment avoids most padding at no cost, so it is a good habit for structs kept in large arrays or copied to a GPU. Packing and bit fields save more space, but they cost speed and portability, so they are worth it only when size matters more than either.

Contact me if you would like to use the contents of this post. Thanks :)
Copyright © 2019 by Gregory Gutmann

Migration notes
  • unpublished WordPress draft (post 438), brought over 2026-10-27