C 구조체 패딩 계산기
C 구조체의 바이트 단위 배치를 계산해 패딩 구멍과 전체 크기를 보여 주고, 크기를 줄이는 멤버 순서를 제안합니다.
long and pointers are 8 bytes.
바이트 맵
Eight bytes per row. Hatched cells are padding the compiler inserts and you cannot use.
멤버
| 오프셋 | 크기 | 정렬 | 종류 | 멤버 |
|---|---|---|---|---|
| 0 | 1 | 1 | uint8_t | type |
| 1 | 3 | — | padding — 3 bytes the compiler inserts | |
| 4 | 4 | 4 | uint32_t | timestamp |
| 8 | 1 | 1 | uint8_t | flags |
| 9 | 1 | — | padding — 1 byte the compiler inserts | |
| 10 | 2 | 2 | uint16_t | length |
| 12 | 3 | 1 | uint8_t | payload[3] |
| 15 | 1 | — | padding — 1 byte the compiler inserts | |
Reordering saves 4 bytes
Declaring members from widest alignment to narrowest removes every interior hole, bringing the struct from 16 bytes down to 12. Only do this where the declaration order is yours to choose — a struct that mirrors a wire format or a hardware register block must keep its layout.
struct Packet {
uint32_t timestamp;
uint16_t length;
uint8_t payload[3];
uint8_t type;
uint8_t flags;
};컴파일러가 빈 바이트를 넣는 이유
프로세서는 대부분의 타입을 자기 크기의 배수 주소에서 읽을 때 가장 빠릅니다. 4바이트 정수는 4의 배수 주소에, 8바이트 double은 8의 배수 주소에 놓이기를 원합니다.
그래서 컴파일러는 멤버 사이에 빈 바이트를 넣어 각 멤버가 알맞은 주소에 오도록 맞춥니다. 그 빈 바이트가
패딩이고, 선언한 멤버 크기의 합보다 sizeof가 큰 이유입니다.
순서가 크기를 바꿉니다
다음 두 구조체는 같은 멤버를 담고 있지만 크기가 다릅니다.
char a; int b; char c;→ 12바이트int b; char a; char c;→ 8바이트
첫 번째는 a 뒤에 3바이트, c 뒤에 3바이트가 버려집니다. 두 번째는 작은
멤버들이 붙어 있어 끝에 2바이트만 붙습니다.
큰 것부터 작은 것 순이 일반적인 규칙입니다. 구조체 하나라면 4바이트 차이가 대수롭지 않지만, 배열로 수천 개를 잡거나 RAM이 몇 KB인 MCU에서는 의미가 다릅니다.
끝에도 패딩이 붙습니다
구조체 전체 크기는 가장 큰 정렬 요구의 배수가 되어야 합니다. 배열로 늘어놓았을 때 두 번째 원소의 첫 멤버도 정렬되어야 하기 때문입니다.
그래서 마지막 멤버 뒤에 아무것도 없어 보여도 크기가 올림될 수 있습니다. 이 계산기의 바이트 맵에서 끝부분의 빈칸이 그것입니다.
packed의 대가
__attribute__((packed))는 패딩을 없앱니다. 크기는 정확히 멤버의 합이 되지만 정렬이
깨집니다.
- 정렬되지 않은 멤버 접근은 여러 명령으로 쪼개져 느려집니다.
- 일부 ARM 구성에서는 정렬되지 않은 접근이 하드웨어 예외를 일으킵니다.
- packed 멤버의 주소를 포인터로 넘기면 그 포인터를 통한 접근이 위험해집니다.
프로토콜 프레임처럼 바이트 배치가 외부에 의해 고정된 경우에만 쓰는 것이 맞습니다. 단순히 크기를 줄이고 싶다면 순서를 바꾸는 쪽이 대가가 없습니다.
구조체를 그대로 보내지 마세요
구조체를 memcpy로 회선에 흘려보내는 코드를 흔히 봅니다. 동작하는 것처럼 보이지만
전제가 많습니다 — 양쪽 컴파일러의 패딩 규칙이 같아야 하고, 엔디언이 같아야 하고, 타입 크기가 같아야
합니다.
패딩 바이트의 내용은 정의되어 있지도 않습니다. 이전에 그 메모리에 있던 값이 그대로 나갈 수 있어 의도치 않은 정보가 새기도 합니다. 회선으로 나가는 데이터는 바이트 단위로 직렬화하는 코드를 따로 쓰는 편이 안전합니다.
ABI를 먼저 고르는 이유
타입의 크기와 정렬 요구가 아키텍처마다 다릅니다. 32비트 ARM에서 long은 4바이트이고
64비트 x86에서는 8바이트입니다. double의 정렬도 32비트 ARM에서는 4바이트, x86-64에서는
8바이트입니다.
그래서 같은 선언이 대상에 따라 다른 크기를 냅니다. 호스트에서 계산한 결과를 타깃에 그대로 적용하면 어긋나므로, 실제로 빌드할 대상을 고르고 확인하세요.
자주 묻는 질문
멤버 크기를 다 더했는데 sizeof가 더 큽니다.
패딩 때문입니다. 컴파일러는 각 멤버를 자기 정렬 요구에 맞는 주소에 놓아야 해서 사이에 빈 바이트를 넣습니다. char 다음에 int가 오면 3바이트가 버려집니다. 구조체 전체 크기도 가장 큰 정렬 요구의 배수로 맞춰지므로 끝에도 패딩이 붙습니다.
멤버 순서를 바꾸면 정말 작아지나요?
작아집니다. 큰 것부터 작은 것 순으로 배치하면 패딩이 거의 사라집니다. char, int, char 순서는 12바이트가 되지만 int, char, char는 8바이트입니다. 배열로 수천 개를 잡는 구조체라면 의미 있는 차이입니다.
packed를 쓰면 되지 않나요?
크기는 줄지만 대가가 있습니다. 정렬되지 않은 멤버에 접근하는 코드가 느려지고, ARM 같은 일부 아키텍처에서는 정렬되지 않은 접근이 하드웨어 예외를 일으킵니다. 프로토콜 구조체처럼 바이트 배치가 고정돼야 하는 경우에만 쓰고, 일반 구조체는 순서를 바꾸는 편이 낫습니다.
구조체를 그대로 전송해도 되나요?
권하지 않습니다. 패딩 바이트의 내용이 정의되지 않았고, 컴파일러와 아키텍처에 따라 배치가 달라집니다. 엔디언 문제도 있습니다. 회선으로 보낼 데이터는 바이트 단위로 직렬화하는 코드를 따로 쓰는 편이 안전합니다.
ABI마다 결과가 다른 이유는?
타입의 크기와 정렬 요구가 다르기 때문입니다. 32비트 ARM에서 long은 4바이트이고 64비트 x86에서는 8바이트입니다. double의 정렬도 아키텍처마다 다릅니다. 그래서 이 계산기는 대상 ABI를 먼저 고르게 되어 있습니다.