How to Eliminate Struct Padding in C

In the C programming language, compilers automatically insert invisible padding bytes between struct members to align data according to the target architecture's memory requirements. While this default behavior optimizes CPU read and write cycles, it increases memory footprint and disrupts data mapping when interfacing with binary file formats, network protocols, or memory-mapped hardware. This article explains how developers can override default alignment and eliminate struct padding using compiler-specific attributes and pragmas in GCC, Clang, and Microsoft Visual C++ (MSVC), along with best practices to prevent hardware-level performance penalties.

Why Compilers Insert Struct Padding

Modern processors access memory in multi-byte chunks (typically 4 or 8 bytes at a time). To maximize execution speed, the compiler naturally aligns a variable of size \(N\) to an address that is a multiple of \(N\). Consider the following structure:

struct SensorData {
    char id;       // 1 byte
    int timestamp; // 4 bytes
    char status;   // 1 byte
};

On a 32-bit or 64-bit system, the compiler adds 3 bytes of padding after id and 3 bytes after status to ensure 4-byte boundaries. As a result, sizeof(struct SensorData) evaluates to 12 bytes rather than the theoretical minimum of 6 bytes.

Eliminating Padding in GCC and Clang

In GCC and Clang, the standard method to remove padding is the __attribute__((packed)) type attribute. When appended to a struct definition, it instructs the compiler to pack members consecutively, assigning each member an alignment of 1 byte.

struct __attribute__((packed)) SensorData {
    char id;       // 1 byte
    int timestamp; // 4 bytes
    char status;   // 1 byte
};

Alternatively, the attribute can be placed at the end of the type definition:

struct SensorData {
    char id;
    int timestamp;
    char status;
} __attribute__((packed));

With this attribute applied, sizeof(struct SensorData) drops to exactly 6 bytes.

Eliminating Padding in MSVC

Microsoft Visual C++ does not support GNU-style attributes for structure packing. Instead, MSVC utilizes the #pragma pack directive. To prevent configuration changes from leaking into external headers, developers should use the push and pop arguments to scope the packing rule.

#pragma pack(push, 1)
struct SensorData {
    char id;       // 1 byte
    int timestamp; // 4 bytes
    char status;   // 1 byte
};
#pragma pack(pop)

The argument 1 sets the alignment boundary to 1 byte, which strips all padding. The #pragma pack(pop) resets the compiler's alignment rules to the previous state. GCC and Clang also support this pragma for compatibility with MSVC codebases.

Creating a Cross-Platform Solution

To maintain portability across GCC, Clang, and MSVC, define a macro wrapper that applies the appropriate directives conditionally:

#if defined(_MSC_VER)
    #define PACKED_STRUCT_START __pragma(pack(push, 1)) struct
    #define PACKED_STRUCT_END   ; __pragma(pack(pop))
#elif defined(__GNUC__) || defined(__clang__)
    #define PACKED_STRUCT_START struct __attribute__((packed))
    #define PACKED_STRUCT_END   ;
#else
    #define PACKED_STRUCT_START struct
    #define PACKED_STRUCT_END   ;
#endif

PACKED_STRUCT_START FrameHeader {
    unsigned char  version;
    unsigned short length;
    unsigned int   checksum;
} PACKED_STRUCT_END

Trade-Offs and Architectural Risks

While packed structs save space and ensure byte-accurate layouts, they introduce potential drawbacks:

  • Performance Degradation: Accessing unaligned multi-byte values (such as an int situated at an odd memory address) requires multiple CPU instructions, cache line cross-overs, or additional masking operations on architectures like x86.
  • Hardware Alignment Faults: Some processor architectures (including older ARM or MIPS designs) do not support unaligned memory access in hardware. Reading an unaligned member can trigger a hardware exception or crash the program via a bus error (SIGBUS).
  • Pointer Aliasing Issues: Taking the address of an unaligned member (e.g., int *ptr = &data.timestamp;) generates a standard pointer. Passing this pointer to functions expecting a naturally aligned boundary can cause undefined behavior or runtime crashes. Copies of these values should be read into local, aligned variables before pointer manipulation.