본문 바로가기

C++/Study

[C++] Memory Alignment of Struct

C++에서 Struct를 만들다보면,

Memory Alignment를 신경써줘야 메모리 낭비를 줄일 수 있다.

 

문서를 통해 정확히 확인할수는 없었지만,

이것저것 시도해보면서 찾은 나름의 규칙을 정리해보려 한다.

 


아래는 전체 코드이다.

코드를 따라가며 결과를 확인해볼 예정이다.

주석은 본인이 알아보려고 적은 것이어서 주석이 이해가 안 된다면 포스팅을 참고하기 바란다.

#include <stdio.h>

#define printMemorySize(TYPE) \
   printf(#TYPE " Size is %d\n", sizeof(TYPE));

/* Data Alignment of Struct */
// #1 : 변수는 선언된 순서대로 메모리에 저장된다.
#if 0
struct ST1 {
	int i1 = 1;
	int i2 = 2;
	int i3 = 3;
};

struct ST2 {
	int i3 = 3;
	int i2 = 2;
	int i1 = 1;
};

int main() {
	ST1 st1;
	ST2 st2;

	return 0;
}
#endif

// #2 : struct 메모리는 멤버 변수 중 가장 큰 size 단위로 증가한다.
//      -> 남는 공간은 그냥 안 쓴다.(== Padding)
#if 0
struct ST1 {
	char c1 = 0;
};

struct ST2 {
	char c1 = 0, c2 = 0;
};

struct ST3 {
	int i = 0;
	char c = 0;
};

struct ST4 {
	double d = 0;
	char c = 0;
};

struct ST5 {
	double d = 0;
	int i = 0;
	char c = 0;
};

int main() {
	ST1 st1;
	ST2 st2;
	ST3 st3;
	ST4 st4;
	ST5 st5;

	printMemorySize(ST1);   // char가 가장 큰 변수이므로 메모리를 1byte단위로 늘려주면서 멤버 변수를 넣어준다.
					  // 1byte 늘린 후, c1을 넣고 완성한다. 
					  // = 1byte

	printMemorySize(ST2);   // 위와 동일. 
					  // 1byte 늘린 후, c1 넣고. 1byte 늘린 후 c2를 넣고 끝. 
					  // = 2byte

	printMemorySize(ST3);   // int가 가장 큰 변수이므로, 4byte단위로 늘려주면서 만든다.
					  // 4byte 늘린 후, i를 넣고, 4byte 늘린 후, c를 넣고 끝. 3byte는 안 쓴다.
					  // = 8byte

	printMemorySize(ST4);   // double이 가장 큰 변수이므로, 8byte단위로 늘려주면서 만든다.
					  // 8byte 늘린 후, d를 넣고, 8byte 늘린 후, c를 넣고 끝. 7byte는 안 쓴다.
					  // = 16byte

	printMemorySize(ST5);   // 위와 동일.
					  // 8byte 늘린 후, d를 넣고, 8byte 늘린 후, i넣고, c가 들어갈 곳이 있으므로 c를 넣고 끝. 3byte는 안 쓴다.
					  // = 16byte

	return 0;
}
#endif

// #3 : 변수가 저장되는 메모리 시작 주소는 변수 size의 배수이다.
//         double   :   8의 배수 메모리 주소에 저장
//         int      :   4의 배수 메모리 주소에 저장
//         char   :   1의 배수 메모리 주소에 저장

//      -> 변수 선언 순서에 따라 Struct size가 달라질 수 있다.
//      -> 메모리 낭비를 줄이려면, 변수 크기 내림차순으로 선언하면 된다.
#if 0
struct ST1 {
	int i = 0;
	double d = 0;
	char c = 0;
};

struct ST2 {
	int i = 0;
	char c = 0;
	double d = 0;
};

struct ST3 {
	char c = 0;
	double d = 0;
	int i = 0;
};

struct ST4 {
	char c = 0;
	int i = 0;
	double d = 0;
};

struct ST5 {
	double d = 0;
	char c = 0;
	int i = 0;
};

struct ST6 {
	double d = 0;
	int i = 0;
	char c = 0;
};

int main() {
	ST1 st1;
	ST2 st2;
	ST3 st3;
	ST4 st4;
	ST5 st5;
	ST6 st6;

	printMemorySize(ST1);
	printMemorySize(ST2);
	printMemorySize(ST3);
	printMemorySize(ST4);
	printMemorySize(ST5);
	printMemorySize(ST6);

	return 0;
}
#endif

// #4 : Struct 내 가장 큰 멤버 변수의 size로 Struct의 메모리 시작주소를 구한다.
#if 0
struct ST1 {
	char c[3];
	ST1() {
		c[0] = c[1] = c[2] = 0;
	}
};

struct ST2 {
	int i[3];
	ST2() {
		i[0] = i[1] = i[2] = 0;
	}
};

struct STR1 {
	ST1 st1;   // ST1은 char가 가장 큰 변수 : 1의 배수 메모리 주소에서 시작.
	ST2 st2;   // ST2는 int가 가장 큰 변수 : 4의 배수 메모리 주소에서 시작.
};
// -> STR1의 가장 큰 변수는 int라고 취급하고 4byte씩 증가한다.

struct STR2 {
	ST1 st1[2];
	ST2 st2;
};
// ST1의 기준 변수는 char이므로, 1의 배수 메모리에 저장될 수 있다.
// ST2는 4의 배수 메모리에 저장된다.

struct STR3 {
	ST1 st1[4];
	ST2 st2;
};
// ST1 4개를 저장하고 나면, 4의 배수이므로 바로 st2를 저장할 수 있다.

int main() {
	STR1 str1;
	STR2 str2;
	STR3 str3;

	printMemorySize(ST1);
	printMemorySize(ST2);

	printMemorySize(STR1);
	printMemorySize(STR2);
	printMemorySize(STR3);

	return 0;
}
#endif

// #5 : 멤버 변수가 저장될 위치를 정했으면, padding을 포함한 size 그대로 저장한 후, 다음 변수의 시작점을 찾는다.
// 편의 상, 다음과 같이 두고 계산을 해보도록 하자.
//      Base : 가장 큰 변수 사이즈
//      Size : Size
#if 1
// Base : 4byte, Size : 8byte
struct ST1 {
	int i;
	char c[2];

	ST1() {
		i = 0;
		c[0] = c[1] = 0;
	}
};

// Base : 8byte, Size : 16byte
struct ST2 {
	double d;
	int i;

	ST2() {
		d = 0;
		i = 0;
	}
};

// Base : 1byte, Size : 2byte
struct ST3 {
	char c[2];
	ST3() {
		c[0] = c[1] = 0;
	}
};

// Base : 8byte, Size : 24byte
struct STR1 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST2 st2; // Base : 8byte, Size : 16byte
};

// Base : 4byte, Size : 12byte
struct STR2 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST3 st3; // Base : 1byte, Size : 2byte
};

// Base : 8byte, Size : 40byte
struct STRUCT1 {
	STR1 str1;   // Base : 8byte, Size : 24byte
	STR2 str2;   // Base : 4byte, Size : 12byte
};

// Base : 8byte, Size : 40byte
struct STRUCT2 {
	STR2 str2;   // Base : 4byte, Size : 12byte
	STR1 str1;   // Base : 8byte, Size : 24byte
};

int main() {
	STR1 str1;
	STR2 str2;

	STRUCT1 struct1;
	STRUCT2 struct2;
	printMemorySize(ST1);
	printMemorySize(ST2);
	printMemorySize(ST3);

	printMemorySize(STR1);
	printMemorySize(STR2);

	printMemorySize(STRUCT1);
	printMemorySize(STRUCT2);

	return 0;
}
#endif

 


먼저, Memory Size를 확인하기 위해,

#define 함수를 정의하였다.

#include <stdio.h>

#define printMemorySize(TYPE) \
   printf(#TYPE " Size is %d\n", sizeof(TYPE));

 


첫 번째 규칙,

Struct 내의 멤버 변수는 선언된 순서대로 메모리에 저장된다.

#if 1
struct ST1 {
	int i1 = 1;
	int i2 = 2;
	int i3 = 3;
};

struct ST2 {
	int i3 = 3;
	int i2 = 2;
	int i1 = 1;
};

int main() {
	ST1 st1;
	ST2 st2;

	return 0;
}
#endif

 

위는 st1의 메모리이다.

int가 4byte인 것을 감안하면 1, 2, 3 순서대로 저장되어있다.

 

위는 st2의 메모리이다.

이번엔 3, 2, 1 순서대로 저장되어있다.

 

이를 통해 멤버 변수는 순차적으로 메모리에 저장되는 것을 확인할 수 있다.


두 번째 규칙,

struct 메모리는 멤버 변수 중 가장 큰 변수의 size 단위만큼 증가한다.

빈 공간은 쓰레기값으로 padding된다.

struct ST1 {
	char c1 = 0;
};

struct ST2 {
	char c1 = 0, c2 = 0;
};

struct ST3 {
	int i = 0;
	char c = 0;
};

struct ST4 {
	double d = 0;
	char c = 0;
};

struct ST5 {
	double d = 0;
	int i = 0;
	char c = 0;
};

int main() {
	ST1 st1;
	ST2 st2;
	ST3 st3;
	ST4 st4;
	ST5 st5;

	printMemorySize(ST1);   // char가 가장 큰 변수이므로 메모리를 1byte단위로 늘려주면서 멤버 변수를 넣어준다.
					  // 1byte 늘린 후, c1을 넣고 완성한다. 
					  // = 1byte

	printMemorySize(ST2);   // 위와 동일. 
					  // 1byte 늘린 후, c1 넣고. 1byte 늘린 후 c2를 넣고 끝. 
					  // = 2byte

	printMemorySize(ST3);   // int가 가장 큰 변수이므로, 4byte단위로 늘려주면서 만든다.
					  // 4byte 늘린 후, i를 넣고, 4byte 늘린 후, c를 넣고 끝. 3byte는 안 쓴다.
					  // = 8byte

	printMemorySize(ST4);   // double이 가장 큰 변수이므로, 8byte단위로 늘려주면서 만든다.
					  // 8byte 늘린 후, d를 넣고, 8byte 늘린 후, c를 넣고 끝. 7byte는 안 쓴다.
					  // = 16byte

	printMemorySize(ST5);   // 위와 동일.
					  // 8byte 늘린 후, d를 넣고, 8byte 늘린 후, i넣고, c가 들어갈 곳이 있으므로 c를 넣고 끝. 3byte는 안 쓴다.
					  // = 16byte

	return 0;
}

 

우선 결과창을 먼저 보도록 하자.

각각의 Struct의 size는 다음과 같다.

 

이제 어떻게 위와 같은 size가 나오게 되었는지 확인해보자.

struct ST1 {
	char c1 = 0;
};

ST1 내에서 가장 큰 변수는 char이다.

따라서, struct의 size를 1byte를 증가시키고, 변수 c1을 담는다.

결과적으로 ST1의 size는 1byte이다.

 

아래는 st1의 메모리이다.

1byte만큼 0으로 바뀐 것을 확인할 수 있다.

 

struct ST2 {
	char c1 = 0, c2 = 0;
};

ST2 내에서 가장 큰 변수는 char이다.

따라서, struct의 size를 1byte를 증가시키고, 변수 c1을 담는다.

다음으로 1byte를 더 증가시키고, 변수 c2를 담는다.

결과적으로 ST2의 size는 2byte이다.

 

아래는 st2의 메모리이다.

2byte만큼 0으로 바뀐 것을 확인할 수 있다.

 

struct ST3 {
	int i = 0;
	char c = 0;
};

ST3 내에서 가장 큰 변수는 int이다.

따라서, struct의 size를 4byte를 증가시키고, 변수 i를 담는다.

다음으로 4byte를 더 증가시키고, 변수 c를 담는다.

결과적으로 ST3는 총 8byte가 된다.

 

아래는 st3의 메모리이다.

앞의 4byte는 i를 의미하고, 뒤에 1byte는 c.

그리고 남는 3byte는 그냥 쓰레기값이다.

 

struct ST4 {
	double d = 0;
	char c = 0;
};

ST4 내에서 가장 큰 변수는 double이다.

따라서, struct의 size를 8byte를 증가시키고, 변수 d를 담는다.

다음으로 8byte를 더 증가시키고, 변수 c를 담는다.

결과적으로 ST4는 총 16byte가 된다.

 

아래는 st4의 메모리이다.

앞의 8byte는 d를 의미하고, 뒤에 1byte는 c.

그리고 남은 7byte는 그냥 padding된 쓰레기값이다.

 

struct ST5 {
	double d = 0;
	int i = 0;
	char c = 0;
};

ST5 내에서 가장 큰 변수는 double이다.

따라서, struct의 size를 8byte를 증가시키고, 변수 d를 담는다.

다음으로 8byte를 더 증가시키고, 변수 i를 담는다.

아직 4byte가 남았기 때문에 굳이 memory를 증가시킬 필요 없이 변수 c를 담는다.

결과적으로 ST5는 총 16byte가 된다.

 

아래는 st5의 메모리이다.

앞의 8byte는 d, 4byte는 i, 1byte는 c.

그리고 남은 3byte는 padding된 쓰레기값이다.

 

이렇게 sturct의 size가 어떤 단위로 증가하는지 확인할 수 있다.


세 번째 규칙,

변수의 메모리 시작 주소는 변수 size의 배수이다.

...이 규칙 때문에 memory alignment를 신경써줘야한다.

 

코드 및 실행 결과를 먼저 확인해보자.

struct ST1 {
	int i = 0;
	double d = 0;
	char c = 0;
};

struct ST2 {
	int i = 0;
	char c = 0;
	double d = 0;
};

struct ST3 {
	char c = 0;
	double d = 0;
	int i = 0;
};

struct ST4 {
	char c = 0;
	int i = 0;
	double d = 0;
};

struct ST5 {
	double d = 0;
	char c = 0;
	int i = 0;
};

struct ST6 {
	double d = 0;
	int i = 0;
	char c = 0;
};

int main() {
	ST1 st1;
	ST2 st2;
	ST3 st3;
	ST4 st4;
	ST5 st5;
	ST6 st6;

	printMemorySize(ST1);
	printMemorySize(ST2);
	printMemorySize(ST3);
	printMemorySize(ST4);
	printMemorySize(ST5);
	printMemorySize(ST6);

	return 0;
}

위의 struct는 모두 double, int, char 변수를 하나씩 가지고 있다.

차이가 있다면, 변수의 선언 순서를 다르게 하였다.

오직 선언된 순서 차이만으로도 sturct의 memory size가 달라지는데... 하나씩 확인해보자.

 

공통점은 모든 struct에서 가장 큰 변수는 double이기 때문에

변수를 담을 메모리 공간이 부족하다면 8byte씩 증가하게 될 것이다.

 

struct ST1 {
	int i = 0;
	double d = 0;
	char c = 0;
};

int를 담기 위해 8byte를 늘린 뒤, 변수 i를 담는다.

int는 4byte이기 때문에 struct 메모리 시작점을 0이라고 한다면, 

다음 변수가 쓰여질 수 있는 메모리 시작점은 4가 될 것이다.

 

여기서 3의 규칙에 따라 double은 8의 배수의 메모리에서 시작할 수 있다.

따라서 메모리 공간이 부족하기 때문에 8byte를 다시 늘려주고,

메모리 시작점을 8로 이동시킨뒤 변수 d를 담는다.

 

다음 char 변수인 c를 담을 공간이 없으므로 또 다시 8byte를 늘려준다.

그리고 변수 c를 담는다.

 

이렇게 8byte를 총 3번 늘렸으므로 ST1의 size는 24byte가 된다.

 

아래는 st1의 메모리이다.

앞의 4byte에 변수 i, 그리고 4byte를 padding 시켜 뛰어넣고,

8byte 변수 d, 1byte 변수 c를 확인할 수 있다.

뒤의 7byte는 padding된 쓰레기값이다.

 

이 규칙대로 나머지도 확인해보자.

 

struct ST2 {
	int i = 0;
	char c = 0;
	double d = 0;
};

8byte를 늘려준 뒤, 변수 i를 담는다.

다음 메모리 시작점은 4이다.

 

char 변수는 size가 1이므로 1의 배수의 메모리에서 시작할 수 있다.

즉, 어디서든 시작할 수 있다.

그럼 4의 위치에 변수 c를 저장하고, 다음 메모리 시작점은 5가 된다.

 

double은 size가 8이므로 8의 배수 메모리에서 시작한다.

5에서 시작할 수 없으므로 8byte를 증가시킨 뒤, 8의 위치에 변수 d를 저장한다.

 

결과적으로 ST2는 총 16byte가 된다.

 

아래는 st2의 메모리이다.

순서대로 변수 i, 변수 c, 그리고 padding된 3byte와

마지막으로 변수 d가 저장된 것을 확인할 수 있다.

 

struct ST3 {
	char c = 0;
	double d = 0;
	int i = 0;
};

char 변수를 담기 위해 8byte를 늘려주고 변수 c를 저장한다.

메모리 시작점은 1이므로 8byte를 늘린 뒤, 8의 시작점에서 변수 d를 저장한다.

뒤에 남는 공간이 없기 때문에 다시 8byte를 늘린 뒤, 변수 i를 저장한다.

 

결과적으로 ST3는 총 24byte가 된다.

 

아래는 st3의 메모리이다.

변수 c 1byte, padding 7byte,

변수 d 8byte, 변수 i 4byte. 그리고 padding 4byte를 확인할 수 있다.

 

struct ST4 {
	char c = 0;
	int i = 0;
	double d = 0;
};

8byte를 늘린 뒤, 변수 c를 저장한다.

변수 i는 4의 배수 메모리 주소에서 시작해야하므로 3byte를 padding시킨 뒤 i를 저장한다.

남은 메모리가 없으므로 8byte를 늘리고 변수 d를 저장한다.

 

따라서, ST4는 총 16byte가 된다.

 

아래는 st4의 메모리이다.

변수 c, padding, 변수 i와 변수 d로 이루어진 것을 확인할 수 있다.

 

struct ST5 {
	double d = 0;
	char c = 0;
	int i = 0;
};

8byte를 늘린 뒤, 변수 d를 넣는다.

남는 공간이 없으므로 다시 8byte를 늘리고 변수 c를 넣는다.

이제 메모리 시작점은 9인데 다음 변수가 int이므로 3byte를 padding시킨 뒤, 변수 i를 넣는다.

 

ST5는 총 16byte가 된다.

 

아래는 st5의 메모리이다.

변수 d, 변수 c, padding 그리고 변수 i를 확인할 수 있다.

 

struct ST6 {
	double d = 0;
	int i = 0;
	char c = 0;
};

드디어 마지막....

8byte를 증가시킨 뒤, 변수 d를 담는다.

남는 공간이 없으므로 다시 8을 증가시킨다.

메모리 시작점은 8이 되고, 8은 4의 배수이기 때문에 바로 변수 i를 담는다.

메모리는 아직 4byte남아있고, 다음 변수는 char이기 때문에 바로 12위치에 변수 c를 담는다.

 

ST6는 총 16byte가 된다.

 

아래는 st6의 메모리이다.

순서대로 변수 d, i, c. 그리고 padding 3byte이다.

 

memory alignment로 검색해보면, 보통 여기까지 설명이 되어있고

"그냥 변수 size 내림차순으로 선언하면 된다" 정도로 마무리된다.

 

본인은 struct 내에 struct가 담겨있는 경우가 궁금해서 조금 더 진행해보았다.


네 번째 규칙,

struct의 멤버 변수로 struct ST가 선언되었을 경우,

ST 내의 가장 큰 멤버 변수의 size의 배수가 ST의 시작주소가 된다.

 

struct ST1 {
	char c[3];
	ST1() {
		c[0] = c[1] = c[2] = 0;
	}
};

struct ST2 {
	int i[3];
	ST2() {
		i[0] = i[1] = i[2] = 0;
	}
};

struct STR1 {
	ST1 st1;   // ST1은 char가 가장 큰 변수 : 1의 배수 메모리 주소에서 시작.
	ST2 st2;   // ST2는 int가 가장 큰 변수 : 4의 배수 메모리 주소에서 시작.
};
// -> STR1의 가장 큰 변수는 int라고 취급하고 4byte씩 증가한다.

struct STR2 {
	ST1 st1[2];
	ST2 st2;
};
// ST1의 기준 변수는 char이므로, 1의 배수 메모리에 저장될 수 있다.
// ST2는 4의 배수 메모리에 저장된다.

struct STR3 {
	ST1 st1[4];
	ST2 st2;
};
// ST1 4개를 저장하고 나면, 4의 배수이므로 바로 st2를 저장할 수 있다.

int main() {
	STR1 str1;
	STR2 str2;
	STR3 str3;

	printMemorySize(ST1);
	printMemorySize(ST2);

	printMemorySize(STR1);
	printMemorySize(STR2);
	printMemorySize(STR3);

	return 0;
}

 

위의 규칙을 활용하면,

ST1과 ST2의 메모리 구조는 쉽게 확인할 수 있으니 생략하도록 하자.

 

struct STR1 {
	ST1 st1;   // ST1은 char가 가장 큰 변수 : 1의 배수 메모리 주소에서 시작.
	ST2 st2;   // ST2는 int가 가장 큰 변수 : 4의 배수 메모리 주소에서 시작.
};

ST1의 size는 3이지만, 멤버 변수 중 가장 큰 변수가 char이기 때문에 char의 특성을 가진다고 보면된다.

즉, 1의 배수 메모리 주소에서 시작하여 총 3byte를 차지한다.

 

ST2의 size는 12지만, int가 가장 큰 멤버 변수기 때문에 int의 특성을 가진다고 본다.

즉, 4의 배수 메모리 주소에서 시작하여 총 12byte를 차지한다.

 

그럼 STR1은 어떻게 될까?

ST2의 size가 12byte이지만, 12byte씩 증가하지는 않는다.

ST1은 char, ST2는 int의 특성을 가진다고 했는데 여기서 활용된다.

STR1 내에서도 가장 큰 변수가 int라고 보게되어 4byte씩 증가하게 된다.

 

먼저 4byte가 증가하고 st1 3byte가 저장된다.

st2는 4의 배수에서 시작하여야하기 때문에 1byte padding시킨 뒤, 4에서 시작하여 12byte를 차지한다.

따라서, STR1은 총 16byte이다.

 

다음은 str1의 메모리이다.

st1 3byte, padding 1byte,

그리고 st2 12byte를 확인할 수 있다.

 

struct STR2 {
	ST1 st1[2];
	ST2 st2;
};

위의 규칙대로 다른 struct도 채워보자.

4byte를 늘린 뒤 st1[0]을 채운다.

1byte가 남았는데, 3byte가 필요하기 때문에 4byte를 증가시킨다.

st1은 1의 배수 메모리에서 시작할 수 있기 때문에 st1[0] 뒤에 바로 st1[1]이 채워진다.

 

여기까지하면, 총 8byte 중 연속된 6byte에 st1[0], st1[1]이 저장되어있다.

다음 메모리 시작점은 6이므로 2byte를 padding 시킨 뒤, 8에서부터 st2를 저장한다.

따라서, STR2는 총 20byte이다.

 

아래는 str2의 메모리이다.

앞에 연속된 6byte가 st1[0], st1[1]이고,

2byte padding 후 st2가 저장되어 있다.

 

 

struct STR3 {
	ST1 st1[4];
	ST2 st2;
};

st1 4개를 저장하기 위해 4byte씩 총 3번 늘린다.

12byte를 써서 st1 4개를 저장하면, 다음 메모리 시작점은 12가 된다.

따라서, st2가 바로 저장될 수 있으며 그대로 12byte를 더 늘려 st2를 저장한다.

따라서, STR3은 총 24byte이다.

 

아래는 str3의 메모리이다.

padding 없이 순서대로 변수가 저장된 것을 확인할 수 있다.


5번째 규칙...은 사실 규칙이라기 보단 정리에 가깝다.

struct가 여러개 선언되어있을 경우, padding된 자리를 활용하는지 궁금했는데

확인해보니 사용하진 않더라...

 

어쨌든, 편의상 base와 size로 정의해서 struct 메모리 구조를 계산해보았다.

size는 말그대로 몇 byte인지 의미하고,

base는 struct 내 가장 큰 변수의 size 정도로 생각하면 될 것 같다.

예제를 보면 이해할 수 있을 것이다.

 

위의 용어를 활용하여 5번째 규칙을 정의하자면...

" 멤버 변수의 base로 변수의 메모리 시작 주소를 구하고, size만큼 메모리를 차지한다.

또한, 멤버 변수의 최대 base가 struct의 메모리 증가량이 된다. "

정도로 할 수 있겠다...

 

* 편의 상 사용한 단어이니 다른 곳에서는 이해 못할 확률이 99.99%입니다... *

** 위를 의미하는 용어가 존재한다면 알려주시면 감사드리겠습니다. **

// Base : 4byte, Size : 8byte
struct ST1 {
	int i;
	char c[2];

	ST1() {
		i = 0;
		c[0] = c[1] = 0;
	}
};

// Base : 8byte, Size : 16byte
struct ST2 {
	double d;
	int i;

	ST2() {
		d = 0;
		i = 0;
	}
};

// Base : 1byte, Size : 2byte
struct ST3 {
	char c[2];
	ST3() {
		c[0] = c[1] = 0;
	}
};

// Base : 8byte, Size : 24byte
struct STR1 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST2 st2; // Base : 8byte, Size : 16byte
};

// Base : 4byte, Size : 12byte
struct STR2 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST3 st3; // Base : 1byte, Size : 2byte
};

// Base : 8byte, Size : 40byte
struct STRUCT1 {
	STR1 str1;   // Base : 8byte, Size : 24byte
	STR2 str2;   // Base : 4byte, Size : 12byte
};

// Base : 8byte, Size : 40byte
struct STRUCT2 {
	STR2 str2;   // Base : 4byte, Size : 12byte
	STR1 str1;   // Base : 8byte, Size : 24byte
};

int main() {
	STR1 str1;
	STR2 str2;

	STRUCT1 struct1;
	STRUCT2 struct2;
	printMemorySize(ST1);
	printMemorySize(ST2);
	printMemorySize(ST3);

	printMemorySize(STR1);
	printMemorySize(STR2);

	printMemorySize(STRUCT1);
	printMemorySize(STRUCT2);

	return 0;
}

자체 정의한 용어는 부끄럽긴하지만, 일단 진행해보자.

 

// Base : 4byte, Size : 8byte
struct ST1 {
	int i;
	char c[2];

	ST1() {
		i = 0;
		c[0] = c[1] = 0;
	}
};

ST1의 Base는 int인 4byte가 되고, 총 Size는 8byte가 된다.

 

// Base : 8byte, Size : 16byte
struct ST2 {
	double d;
	int i;

	ST2() {
		d = 0;
		i = 0;
	}
};

ST2의 Base는 double인 8byte, 총 Size는 16byte가 된다.

 

// Base : 1byte, Size : 2byte
struct ST3 {
	char c[2];
	ST3() {
		c[0] = c[1] = 0;
	}
};

ST3의 Base는 1byte, 총 Size는 2byte이다.

 

// Base : 8byte, Size : 24byte
struct STR1 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST2 st2; // Base : 8byte, Size : 16byte
};

STR1의 Base는 ST1과 ST2의 Base의 최대값인 8byte가 된다.

즉, STR1은 8byte씩 증가하게 된다.

 

메모리 구조를 예측해보자면,

8byte가 증가하고, st1이 8byte를 차지하게 된다.

이제 메모리 시작 주소가 8이므로 padding없이 바로 st2 16byte가 저장될 것이다.

따라서, STR1은 총 24byte가 된다.

 

다음은 str1의 메모리이다.

처음에 st1의 메모리 구조가 그대로 들어가있고,

다음으로는 st2의 메모리 구조가 그대로 들어가있다.

 

// Base : 4byte, Size : 12byte
struct STR2 {
	ST1 st1; // Base : 4byte, Size : 8byte
	ST3 st3; // Base : 1byte, Size : 2byte
};

STR2의 Base는 ST1과 ST3의 Base 최대값인 4byte가 된다.

즉, 4byte씩 증가하면서 st1과 st3를 채워줄 것이다.

 

먼저 (4 * 2)byte를 늘려 st1을 채운다.

ST3의 Base는 1이므로 어느 메모리 주소에서든 시작이 가능하고, 2byte만 더 있으면 된다.

4byte를 더 늘리고, st3를 채워준다.

남는 2byte는 padding으로 채워질 것이다.

 

따라서, STR2는 총 12byte가 된다.

 

아래는 str2의 메모리이다.

ST1의 메모리 구조와 ST3의 메모리 구조가 순서대로 들어가있고,

2byte padding되어있다.

 

ST1의 padding된 2byte에 ST3가 들어갈 수 있다면 좋겠지만...

struct 에서 padding된 메모리를 서로 사용할수는 없는 것 같다.

 

// Base : 8byte, Size : 40byte
struct STRUCT1 {
	STR1 str1;   // Base : 8byte, Size : 24byte
	STR2 str2;   // Base : 4byte, Size : 12byte
};

이제 STR1과 STR2로 STRUCT1을 만들어보았다.

위에서 구한 Base와 Size를 활용하면, STRUCT1의 Base는 8byte가 된다.

즉, 8byte씩 메모리를 증가시킨다.

 

8byte씩 3번 증가시켜 str1을 채운다.

다음 메모리 시작 주소는 24가 된다.

이는 STR2의 Base의 배수이기 때문에 바로 채워질 수 있다.

따라서, 8byte씩 2번 메모리를 증가시키고 str2를 채운 뒤, 남는 4byte는 padding될 것이다.

 

따라서, STRUCT1은 총 40byte가 된다.

 

다음은 struct1의 메모리이다. 편의상 8byte씩 끊었다.

앞의 3칸은 STR1과 같은 메모리 구조이고,

STR2의 메모리 구조가 채워진 뒤, 4byte padding된 것을 확인할 수 있다.

 

// Base : 8byte, Size : 40byte
struct STRUCT2 {
	STR2 str2;   // Base : 4byte, Size : 12byte
	STR1 str1;   // Base : 8byte, Size : 24byte
};

드디어 정리한 마지막 예제...

STRUCT1의 변수 선언 순서를 바꾸었다.

 

Base는 8byte이므로 8byte씩 증가한다.

12byte를 채워주기 위해 8byte * 2인 16byte를 증가시키고 str2를 채워준다.

이제 메모리 시작 주소는 12이지만, STR1의 base는 8이기 때문에 8의 배수에서 시작해야한다.

따라서, 4byte를 padding시킨 뒤, 8byte * 3만큼 size를 증가시켜 str1를 채워준다.

 

따라서, STRUCT2의 총 size는 40byte가 된다.

 

다음은 struct2의 메모리이다. 이번에도 그냥 8byte로 끊었다.

12byte의 STR2의 메모리 구조, 4byte padding, 24byte만큼 STR1의 메모리 구조를 확인할 수 있다.


memory alignment에 대해 제대로된 문서를 구할 수가 없어서

여러 상황을 만들어가며 유추해보았다.

 

어디 가서 배운게 아니다보니 정확하게 이 규칙이 맞는지는 확신할 수 없지만,

그래도 위의 5가지 규칙을 따랐을 때, 시도해본 모든 경우의 메모리 구조를 예측할 수 있었다.

 

본 포스팅에서는 문장으로 명시하기 위해

자체 정의한 base라는 용어를 사용하였는데, 문제의 소지가 있다면 알려주시면 감사드리겠습니다..!

 

* Visual Studio 2017 community C++로 작성함

'C++ > Study' 카테고리의 다른 글

[C++] Type Conversion, 자동 형변환  (0) 2022.12.07