C언어 __attribute__(pack), 구조체 정렬
바이트 패딩과 attribute((packed))에 대한 이해
C언어에서 구조체를 다룰 때
구조체의 크기(Size)는 단순히 각 멤버 변수들의 크기를 더한 값으로 결정되지 않습니다.
이는 바이트 패딩이라는 개념 때문입니다.
이번 글에서는 바이트 패딩과 이를 피할 수 있는 방법인 attribute((packed))에 대해 알아보겠습니다.
바이트 패딩이란?
구조체를 구성하는 멤버 변수들은 메모리에 연속적으로 저장됩니다.
그러나 컴파일러는 사용자 환경(32비트, 64비트)에 따라 성능 최적화를 위해
멤버 변수들 사이에 추가적인 빈 공간(패딩)을 삽입할 수 있습니다.
이 패딩은 데이터 접근 속도를 높이기 위한 것으로
1바이트씩 계산하는 것보다 4바이트(32비트 환경), 8바이트(64비트 환경) 단위로 처리하는 것이
컴파일러 입장에서 더 효율적이기 때문입니다.
이를 비유하자면, 타코야키를 구울 때 1구짜리 철판 대신 4구짜리 철판을 사용하는 것과 같습니다. 1개만 만들더라도 4구짜리를 사용한다면 나머지 공간이 비어도 어차피 4구짜리를 사용하는 셈이죠.
예시 코드
#include <stdio.h>
struct TEST {
char a; // 1바이트
int b; // 4바이트
};
int main() {
printf("Size of TEST: %lu\n", sizeof(struct TEST));
return 0;
}
위 코드를 실행하면 struct TEST의 크기는 5바이트가 아닌 8바이트로 출력됩니다.
이는 char a와 int b 사이에 3바이트의 패딩이 삽입되기 때문입니다.
패딩 메커니즘
컴파일러는 구조체의 멤버 변수를 메모리에 배치할 때, 멤버 변수의 크기를 기준으로 정렬합니다. 아래는 struct TEST의 메모리 배치를 설명한 것입니다:
- char a: 1바이트 차지, 이후 남은 공간은 3바이트 패딩.
- int b: 4바이트 차지, 주소가 4의 배수로 정렬됨.
이로 인해 총 크기는 8바이트가 됩니다. 이러한 구조체 패딩은 CPU가 데이터를 효율적으로 읽고 쓰는 데 도움을 줍니다.
attribute((packed))의 사용
구조체 크기를 최소화해야 하는 상황에서는 패딩을 제거할 수 있습니다.
이를 위해 __attribute__((packed))를 사용합니다.
이 속성은 컴파일러에게 멤버 변수를 빈 공간 없이 연속적으로 배치하라고 지시합니다.
특히, 통신 프로토콜에서 데이터가 바이트 단위로 정확히 정렬되어야 할 때 유용합니다. (일반적으로 하나하나 짤라쓰니깐)
#include <stdio.h>
struct TEST {
char a; // 1바이트
int b; // 4바이트
} __attribute__((packed));
int main() {
printf("Size of packed TEST: %lu\n", sizeof(struct TEST));
return 0;
}
위 코드에서 __attribute__((packed))를 사용하면 구조체의 크기는 5바이트로 출력됩니다.
패딩이 제거되었기 때문입니다.
하지만 용량을 줄인다고 해서 마냥 좋은 것은 아닙니다.
위에서 바이트패딩을 컴파일러가 하는 이유는 속도적인 측면에서 느려지기 때문인데
패딩을 제거하면 메모리 접근 시 비정렬 데이터로 인해 성능이 저하될 수 있습니다.
CPU는 정렬되지 않은 데이터를 처리하기 위해 추가 작업을 수행해야 할 수 있습니다.
이번에 이 글을 정리하게 된 것도 모듈 간 선언된 헤더에서 한 쪽은 __attribute__((pack))이 되어 있지 않아
특정 헤더 부분이 짤려서 생긴 이슈가 있었기 때문입니다.
이렇듯 바이트 패딩으로 인해 프로토콜 헤더의 위치가 바뀔 수도 쓰레기 값이 들어갈 수도 있음을 잘 생각하며
필요에 따라 어트리뷰트를 사용해야 합니다.