구조체 멤버 맞춤(struct member alignment) C++

아시는 분들도 계시고 모르시는 분들도 계실테지만 오늘은 구조체에 대한 내용을 분석해 보려합니다. 구조체는 일종의 데이터의 집합입니다. 그리고 클래스가 구조체의 업그레이드라고 아시고 계실 겁니다. 따라서 여기서 다루는 내용은 클래스에도 해당되는 내용입니다.

구조체 멤버 맞춤이란 구조체에 선언되는 멤버들이 메모리에 할당될 때 어떻게 할당되는가를 말합니다. 비유하자면 필통에 연필, 자, 지우개 등을 넣을 때 어떤 순서로 어떻게 넣는지에 대한 것과 비슷합니다. 

데이터 구조 정렬의 정의[각주:1]

우선 한가지 실험을 먼저 해보겠습니다. Visual Studio c++ 환경입니다.


구조체의 크기 실험



▼다음은 위 코드의 결과입니다.

코드에 정위된 두 구조체는 멤버의 선언 순서만 다르지 멤버 구성은 동일합니다. 그럼에도 구조체의 크기는 차이를 보입니다.

구조체를 선언할 때는 선언 순서에 주의해야 합니다. 앞에서는 동일한 세 개의 멤버를 선언했음에도 8byte 차이가 납니다. 작은 차이 같지만 위 구조체를 많이 사용할 수록 이 차이는 점점 벌어지겠죠. 


구조체 멤버의 선언 순서


왜 이런 차이가 날까요? 구조체가 메모리에 저장될 때는 멤버들이 순서대로 구조체의 주소로 부터 offset 을 가집니다. offset은 배열로 따지면 색인(index)와 같습니다. 멤버마다 자신만의 offset이 있고 이 offset을 통해서 멤버의 위치를 알아낼 수 있습니다. 우리가 offset을 다룰 필요는 없고 우리는 단지 S1.a, S1.b와 같은 식으로 멤버 변수에 접근하면 자동으로 해당 offset만큼 떨어진 주소로 가서 멤버의 값을 읽어내는 것이죠.

위 두 구조체는 멤버들의 구성은 같지만 선언 순서로 인해 offset이 다르게 된건 알 수 있습니다. 하지만 왜 크기까지 바뀐지는 설명되지 않네요. 

그럼 우선 두 구조체를 분석해서 offset이 어떻게 정해지는지를 알아보겠습니다.




위 그림은 두 구조체가 메모리에 저장된 모습입니다. 그림을 보면 느낌이 오신 분들도 있으실 겁니다. 잘 보면 8byte 단위로 경계지어짐을 알 수 있습니다. 멤버들이 순서대로 저장되는데 8byte의 가방에 나눠 담는 것 같죠. 예전에 등산을 할 때 어머니가 먹을 것을 특정 부피 단위로 나누어서 봉지에 담아주신 기억이 나네요. 뭐 이해하시기 편하시다면 8byte가방을 여러개 두어서 멤버들을 나눠 담는다고 생각하시면 됩니다.

그런데 문제는 멤버들의 순서입니다. 컴퓨터는 원래 "바보"라서 사람처럼 생각하지 않습니다. 위쪽에 있는 그림에선 char형 다음에 double형이 왔을 때 이전에 char형을 넣어둔 가방은 닫아버렸습니다. 뒤쪽에 int형이 왔는데도 말이죠. 사람이라면 int형이 4byte이니까 앞에서 char형을 넣을 때 사용한 가방을 사용하겠죠? 그런데 컴퓨터는 "바보" 아니면 "부자"입니다. 왜냐면 새가방을 마련해서 int형을 넣으니까요.

그런데 밑에 있는 그림은 char형 다음 int형이 왔을 때 같은 가방에 넣네요. 역시 컴퓨터는 "부자"가 아니라 "바보"였습니다.

따라서 구조체에 멤버를 선언할 때 순서는 중요합니다. 일반적으로 사이즈가 작은 것부터 순서대로 선언해주시면 되겠죠?


사실은 단순하지 않다


위 설명은 단순한 설명입니다. 사실 멤버들이 메모리에 자리잡는 과정은 좀 신경써야할 부분이 있습니다. 그렇다고 우리가 일일히 다 신경쓸 필요는 없고 사이즈가 작은 멤버부터 순서대로 선언해 주면 되겠습니다.

이제 구조체 멤버가 메모리에 할당되는 방식을 더 파해칠 건데 이 부분은 순수한 탐구 목적이므로 신경쓰지 않으셔도 됩니다.^^

다음 코드에서 두 구조체의 사이즈를 비교해 보세요.


처음 선언한 구조체의 멤버는 순서대로 1byte char형 4개, 4byte int형 1개, 8byte double형 1개 총 16byte가 되겠네요.

두 번째 구조체 역시 1byte char형 2개 4byte형 1개 1byte char형 2개가 8byte로 하나의 가방에 들어가겠군요. 16byte가 되겠네요.~

하지만 결과?는 왜 아래 것이 24byte가 되었을까요? 죄송합니다. 제가 사기친게 아닙니다.  

가방에 넣는 몇가지 규칙이 더 있기 때문입니다.

좀 설명하기 어려운데 설명 드려보겠습니다. 

가방에 들어갈 멤버들 중에 가장 큰 사이즈를 갖는 멤버의 사이즈로 가방안의 경계가 지어진다.

지금까지 가방이 8byte 크기의 공간이었죠?

위 코드에서 이 가방 안에 들어갈 녀석들은 char형 4개와 int형 1개 였습니다. int형이 이 가방 내에서는 4byte로 가장 큰 녀석이네요. 그럼 이 가방에 4byte의 경계가 다시 지어집니다. 즉 이 가방은 4byte의 공간을 2개 갖는 가방이 되는 거죠. 예를 들어 이 가방에 들어갈 멤버 중 short형이 가장 큰 녀석이었다면 이 가방은 2byte공간 4개를 가진 가방으로 변신하겠죠. 



따라서 MyData1구조체는 위 그림처럼 멤버가 들어가게 됩니다.


위 그림은 MyData2대한 그림입니다. 컴퓨터는 "바보"라서 c와 d는 순서대로 다음 가방에 넣습니다. 앞에 빈 2byte의 공간은 그냥 계속 빈 공간으로 남는거죠. 그리고 double형 역시 새 가방에 넣겠죠. 여기서도 6byte의 낭비가 있네요. 그래서 24byte를 차지하게 됩니다.


멤버 맞춤 변경


프로그래머가 이 구조체 멤버 맞춤을 변경할 수 있습니다. 우선 앞에는 기본으로 8byte를 경계로 맞춤되었습니다. 이건 64비트 환경에서고 32비트 환경에서 개발한다면 기본 4byte지만 앞에서 설명한 것과 같은 맥락으로 전체 멤버 중에 가장 사이즈가 큰 멤버의 크기를 기준으로 맞춤 경계가 줄어들 수 있습니다. 앞에서는 double형이 멤버에 포함되어 있었으므로 기본으로 8byte경계를 갖습니다. 그럼 이 경계를 4byte로 바꾸면 어떻게 될까요? 경계의 크기를 바꾸는 방법은 몇가지가 있는데 우선 코드 내에서 다음과 같이 바꿀 수 있습니다.

구조체의 선언 전에 #pragma pack(4)라는 선언이 추가되었습니다.

"가방의 사이즈를 4byte로 바꿔라." 입니다.  double이 있는데 가능한가요?라고 할 수도 있겠네요. 실행보니 가능하더라구요. 제가 분석한 결과는 double은 부득이하게 8byte 가방으로 준비하고 나머지는 4byte 가방에 넣습니다. 즉 위 구조체는 앞에서는 24byte의 크기였지만 pack(4)의 결과 20byte로 줄어듭니다. 메모리 공간에 4, 4, 4, 8 의 총 20byte의 크기로 가방이 생기고 순서대로 char 2개, int 1개, char 2개, double 1개가 들어갑니다. 


그런데 #pragma pack(4)를 해 놓으면 이후의 구조체는 모두 이 선언에 영향을 받습니다. 따라서 다음과 같이 선언을 해주는 것이 좋습니다.

push는 현재 멤버 맞춤 크기를 스택에 저장해 놓으라는 의미입니다. 그리고 1을 현재 맞춤으로 사용합니다.

구조체 정의가 끝난 후 pop을 하면 기존 맞춤 값을 복원합니다.


두 가지 방법이 더 있습니다. 프로젝트에서 오른쪽 마우스 클릭을 한 후 속성을 선택합니다.

구조체 멤버 맞춤에서 직접 선택하면 프로젝트 전체에서 구조체 멤버 맞춤을 할 수 있습니다.


그 밖에도 C++11에서는 MSDN도움말 에서 추가적인 방법을 제공합니다. 이 방법에서 주의할 점은 프로세서의 WORD크기 이하에서는 사용하지 말 것을 당부합니다. 만약 사용하게 되면 구조체 멤버에대한 접근불가한 경우가 생길 수 있기 때문입니다. 실험해 봤는데, 제 컴퓨터에서는 이 방법으로 4byte이하로 설정하면 적용되지 않네요. 참고만 해주세요. 


그러면 메모리 공간을 줄이기 위해서 무조건 1로 해 놓으면 되지 않는가 할 수 있겠습니다. 이 부분은 저도 정확히는 모르지만 효율성에 있는 것 같습니다. 굳이 설명해 본다면 CPU는 WORD[각주:2]라는 단위로 데이터를 캐시에서 읽어드립니다. 이 때 읽어드리는 데이터와 데이터 사이의 경계가 생기는데 이 경계가 불규칙해 지면 성능상의 저하가 옵니다. 예를 들어 배열은 하나의 타입만 요소로 갖습니다. 만약 int형 배열이라면 4byte단위로 요소간 경계가 지어졌겠죠. 이런 규칙성으로 인해서 CPU의 접근 성능이 좋습니다. 비유하자면 땅에 한걸음 간격으로 표시를 하는건 쉽겠죠. 하지만 한걸음 한뼘, 한걸음 두뼘반 이렇게 섞여 있다면 작업 효율이 떨어질 겁니다.


참고 페이지

위 사이트는 이에 대한 토론이군요. 꽤 심도있게 토론합니다. (어서 영어공부를 해야지 알려주실분 ~ ㅠㅠ)


앞에서 4byte로 데이터 경계(align)를 만들었을 때 double형이 있더라도 8byte로 되었죠. 아마 8은 4의 배수이므로 성능상 큰 하락이 없을 겁니다. 비유하자면 4byte는 작은 걸음 8byte는 작은걸음 두번 정도의 성능 하락이겠죠. cpu입장에선 포인터 연산을 위해 경계를 찾는 것이 관건입니다. 따라서 이 경계 일정하게 설정해 놓는 것이 참 중요한 일이죠.


그렇다고 무조건 모두 8byte로 해놓느냐? 하는 것도 아닙니다. 언제나 성능과 메모리 효율은 적절한 수준을 유지하는 것이 좋으니까요.

이건 전적으로 설계하는 프로그래머의 경험에 달려있다고 생각합니다. 얼마나 해당 데이터가 자주 사용되느냐가 관건이겠죠. 몇번 사용되지 않는 것이라면 1byte 단위로 맞춤(align)해도 큰 문제 없을 겁니다.

어쨌든 가능하면 이 부분은 건드리지 않는게 좋습니다. 기본 설정이 제일 좋다고 생각되네요. ^^

  1. 메모리에 데이터가 정렬되고 접근되는 방식. 컴퓨터가 메모리 주소를 읽고 쓸 때 WORD크기 또는 그 배수로 수행된다. 데이터 정렬(align)은 WORD크기 또는 그 배수의 OFFSET으로 메모리에 데이터가 저장되는 것을 말한다. CPU의 성능에 영향을 미칠 수 있으므로 데이터의 구조 끝과 다음 데이터 시작 사이에 padding(여분의 공간)이 들어갈 수 있다.(data structure padding) [본문으로]
  2. WORD는 CPU가 데이터를 읽어들이는 단위로 CPU의 아키텍쳐마다 다르다. 64bit컴퓨터라면 WORD는 8byte 32bit컴퓨터라면 WORD는 4byte가 된다. 하지만 Visual c++에서 WORD의 크기는 typedef가 어떻게 되어있는지 직접 확인해야 한다. 일반적으로 2byte로 되어있다. [본문으로]
이 댓글을 비밀 댓글로