Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-15 13:20
관리 메뉴

nomad-programmer

[Programming/C] 구조체 멤버 정렬 (Struct Member Alignment) 본문

Programming/C

[Programming/C] 구조체 멤버 정렬 (Struct Member Alignment)

scii 2020. 6. 14. 23:26

과거에는 컴퓨터 시스템의 메모리 용량이 작아 프로그래머들은 메모리를 최대한 적게 사용하도록 프로그램을 개발했다. 허나 최근 메모리 용량이 점차 늘면서 메모리를 더 사용하더라도 프로그램의 실행 속도가 향상되도록 프로그램을 개발하고 있다.

구조체의 경우 다양한 크기의 메모리를 하나의 그룹으로 묶어 사용하다 보니 구조체 요소를 접근할 때 실행 속도가 떨어지는 문제가 발생한다. 그래서 구조체의 요소를 일정한 크기로 정렬하여 실행 속도를 더 빠르게 하는 개념이 C언어 컴파일러에 추가되었다.

컴파일러마다 용어의 차이는 있지만 마이크로소프트에서 제공하는 C컴파일러의 경우에는 "구조체 멤버 정렬" 기능을 제공하며 1, 2, 4, 8바이트로 정렬 기준을 설정할 수 있다.

#pragma warning(disable: 4996)

#include <stdio.h>

// 구조체를 1바이트 크기로 정렬
#pragma pack(push, 1)

struct Test1 {
	char a;    // 1바이트
	int b;     // 4바이트
	short c;   // 2바이트
	char d;    // 1바이트
};

struct Test2 {
	char a;    // 1바이트
	double b;  // 8바이트
	short c;   // 2바이트
	char d;    // 1바이트
};

// 구조체 1바이트 크기 정렬 해제
#pragma pack(pop)

struct OrgTest1 {
	char a;    // 1바이트
	int b;     // 4바이트
	short c;   // 2바이트
	char d;    // 1바이트
};

struct OrgTest2 {
	char a;    // 1바이트
	double b;  // 8바이트
	short c;   // 2바이트
	char d;    // 1바이트
};

int main(void) {
	struct Test1 t1;
	struct Test2 t2;
	struct OrgTest1 org_test1;
	struct OrgTest2 org_test2;

	printf("Test1: %d bytes\n", sizeof(t1));
	printf("Test2: %d bytes\n", sizeof(t2));
	puts("-----------------------------------------");
	printf("Org Test1: %d bytes\n", sizeof(org_test1));
	printf("Org Test2: %d bytes\n", sizeof(org_test2));

	return 0;
}

// 결과
/*

Test1: 8 bytes
Test2: 12 bytes
-----------------------------------------
Org Test1: 12 bytes
Org Test2: 24 bytes

*/

컴파일러에서 기본값으로 구조체 정렬 기준을 어떻게 설정하는지에 따라 구조체로 만든 자료형의 크기가 달라진다.

그래서 이 기능을 모르고 단순히 구조체를 구성하는 요소의 크기를 합산하여 구조체 크기를 사용하다가 버그가 발생할 수 있으므로 주의해야 한다.

 

* 1바이트 정렬

1바이트 정렬

이 정렬을 사용하면 구조체의 본래 의미대로 메모리가 구성된다. 따라서 이 기준으로 정렬 된 Test 자료형의 크기는 8바이트이다.

 

* 2바이트 정렬

2바이트 정렬

각 요소는 2의 배수에 해당하는 주소에서 시작할 수 있고 전체 크기가 2의 배수가 되어야 한다. 따라서 요소가 놓일 주소가 2의 배수가 아니라면 해당 1바이트를 버리고 2의 배수가 되는 주소에 놓인다.

하지만 예외적으로 요소의 자료형이 2바이트보다 작은 경우에는 해당 요소의 크기로 정렬된다. 예를 들어 2, 4, 8바이트 자료형의 요소들은 2의 배수에 해당하는 주소에 배치되지만 2바이트보다 작은 1바이트 자료형 요소들은 그대로 1바이트로 정렬된다.

위와 같은 기준을 적용하면 전체 크기는 9바이트가 되어야 하지만, 2바이트 정렬은 전체 크기가 2의 배수가 되어야 하기 때문에 10바이트가 된다. 그리고 마지막 1바이트를 사용하지 않는다. 

결론적으로 2바이트 정렬 기준으로 정렬한 Test1 구조체 크기는 "10바이트" 이다.

 

* 4바이트 정렬

4바이트 정렬

각 요소는 4의 배수에 해당하는 주소에서 시작할 수 있고 전체 크기가 4의 배수가 되어야 한다. 따라서 요소가 놓일 주소가 4의 배수가 아니라면 해당 1~3바이트를 버리고 4의 배수가 되는 주소에 놓인다. 

하지만 요소의 자료형이 4바이트보다 작은 경우에는 해당 요소의 크기로 정렬된다. 예를 들어 4바이트와 8바이트 자료형의 요소들은 4의 배수에 해당하는 주소에 배치되지만 4바이트보다 작은 1바이트 자료형은 1바이트 정렬이 적용되고 2바이트 자료형은 2바이트 정렬이 적용된다.

위와 같은 기준을 적용하면 전체 크기는 11바이트가 되어야 하지만, 4바이트 정렬은 전체 크기가 4의 배수가 되어야 하기 때문에 12바이트가 된다. 그리고 마지막 1바이트를 사용하지 않는다. 

결론적으로 4바이트 정렬 기준으로 정렬한 Test1 구조체의 크기는 "12바이트" 이다.

 

* 8바이트 정렬

8바이트 정렬

구조체를 정렬할 때 모든 요소가 기준 정렬 바이트보다 작으면 요소 중에서 가장 큰 소요의 크기로 정렬된다. 따라서 Test1 구조체 요소 중 가장 큰 요소의 크기는 4바이트이므로  8바이트 정렬을 사용해도 4바이트로 정렬되어 버린다. 따라서 Test2 구조체로 예시를 들겠다.

각 요소는 8의 배수에 해당하는 주소에서 시작할 수 있고 전체 크기가 8의 배수가 되어야 한다. 요소가 놓일 주소가 8의 배수가 아니라면 해당 1~7바이트를 버리고 8의 배수가 되는 주소에 놓인다. 하지만 요소의 자료형이 8바이트보다 작은 경우에는 해당 요소의 크기로 정렬된다.

예를 들어 8바이트 자료형 요소들은 8의 배수에 해당하는 주소에 배치되지만 8바이트보다 작은 1, 2, 4바이트 자료형은 각각 1, 2, 4바이트 정렬이 적용된다.

위의 같은 기준을 적용하면 전체 크기가 19바이트가 되어야 하지만, 8바이트 정렬은 전체 크기가 8의 배수가 되어야하므로 크기는 24바이트가 된다. 그리고 마지막 5바이트를 사용하지 않는다.

결론적으로 8바이트 정렬 기준으로 정렬된 Test2 구조체의 크기는 "24바이트" 이다.

 

마지막 8바이트 정렬의 예시를 보면 알 수 있듯이 이 구조체는 24바이트 중 무려 12바이트나 버려진다. 그런데 요즘 컴파일러들은 8바이트 정렬을 기본 값으로 하고 있다.

따라서 구조체에 8바이트 크기의 자료형을 사용하지 않았다면 낭비가 적겠지만, double이나 __int64 같은 8바이트 크기의 자료형을 사용하는 순간 구조체 크기가 갑자기 커지게 될 것이다.

 

구조체의 요소는 같은 크기끼리 모아준다.

구조체로 자료형을 선언할 때 같은 크기의 요소들끼리 모아 주는 것만으로도 프로그램의 효율을 크게 높일 수 있다.

// 기존의 구조체 Test2

struct Test2 {
    char a;    // 1바이트
    double b;  // 8바이트
    short c;   // 2바이트
    char d;    // 1바이트
};
// 개선된 구조체 Test2

struct Test2 {
    char a;    // 1바이트
    char d;    // 1바이트
    short c;   // 2바이트
    double b;  // 8바이트
};

다음은 개선된 구조체의 메모리 배치도이다. 그림을 보면 구조체의 크기가 16바이트이다. 단순히 구조체 요소의 순서만 변경했을 뿐인데 낭비되던 메모리가 12바이트에서 4바이트로 줄었다.

개선된 구조체 메모리 배치도

이처럼 작은 부분이라도 정확하게 개념을 이해하여 소스 코드를 구성하면 특별한 최적화 작업을 해주지 않아도 프로그램의 메모리 사용 효율을 높일 수 있다. 

그리고 앞서 설명한것처럼 컴파일러 설정에 따라 구조체로 선언한 자료형의 크기가 바뀔 수 있다. 따라서 동적 메모리 할당을 할 때 구조체의 크기를 직접 계산하여 사용하는 것보다 자료형의 크기를 계산해 주는 "sizeof 연산자"를 사용하는 것이 안전하다.

// 컴파일러 설정에 따라 오류 발생할 수 있다.
struct Test2 *ptr = (struct Test2*)malloc(16);

// 안전하고 권장하는 형태
struct Test2 *ptr = (struct Test2*)malloc(sizeof(struct Test));
Comments