char data[1]의 역할은?

C/C++ 퀴즈 하나

물론 고수님들에겐 쉬운 문제지만 다음 구조체에서 char data[1]의 역할이 무엇인지 생각 해봅시다.

typedef struct tagWHATTHE {
int data1;
int data2;
char data[1];
} WHATTHE;

모르시는 분들은 최소 1분 정도 생각을 해봅시다.



그래도 잘 모르겠으면 약간의 힌트:

typedef struct tagWHATTHE {
int size;
int type;
char data[1];
} WHATTHE;



char data[1]의 역할은

난생 처음 이런 구조체를 보면 data[1] 변수가 도대체 어떤 의미인지 감을 잡기 힘들다. 왜 이런 변수가 쓰이는지는 직접 예제 코드를 보는 것이 가장 좋을 것 같다.

typedef struct tagHEADER {
int size;
int type;
char data[1];
} HEADER;

// int data_length, int* data;
HEADER* hd = (HEADER*)malloc(sizeof(HEADER) + data_length);
hd->size = sizeof(HEADER) + data_length;
hd->type = 0;
memcpy(hd->data, data, data_length);
fwrite(hd, 1, hd->size, file_handle);

한 마디로 char data[1]의 의미는 길이가 정해지지 않은 데이터를 담기 위한 일종의 더미 변수라고 보면 된다. 예에서도 있듯이 이런 코딩 테크닉은 어떤 데이터의 헤더를 표현할 때 많이 등장한다. 메모리 덩어리를 할당 받고 그것을 이 헤더 타입으로 캐스팅을 하면 앞 부분은 헤더가 놓이고 바로 뒤에 갖고 싶은 가변 길이 데이터를 쉽게 이을 수 있다. 윈도우 프로그래밍을 해보신 분이라면 아마 이런 것을 자주 봤을 것이다. 예를 들면:

typedef struct _RGNDATAHEADER {
DWORD dwSize;
DWORD iType;
DWORD nCount;
DWORD nRgnSize;
RECT rcBound;
} RGNDATAHEADER;

typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA;

RGN이라는 그래픽에서 그리는 영역을 지정하는 클리핑 객체를 담는 자료구조다. RGNDATA에 채워질 데이터가 몇 개나 올지 알 수 없으므로 이렇게 하였다. 또, 대표적인 비트맵 파일 관련 구조체도 빼놓을 수 없다.

typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;

배열 크기가 여기는 [1]인데 [0]으로 둬도 사실 문제 없다. 그리고 소켓 프로그래밍할 때 보게 되는 hostent에도 이제는 과거 호환성 때문에만 남았지만 크기가 0인 배열을 볼 수 있다. 그런데 보통 크기 0인 배열을 선언하면 컴파일러가 워닝을 띄운다. 그래서 C99 표준에는 flexible array member라고 해서 맨 마지막 구조체 변수의 선언이 char data[] 처럼 될 수 있도록 하기도 한다.

도대체 왜?

그렇다면 왜 이렇게 했을까? 보다 직관적으로 코드를 만든다면 아래처럼 쓸 수도 있다.

typedef struct tagHEADER {
int size;
int type;
char* data;
} HEADER;

HEADER* hd = (HEADER*)malloc(sizeof(HEADER));
hd->size = 1024;
hd->type = 0;
hd->data = (char*)malloc(hd.size);
...

그러나 이 접근은 단점이 있다. 위에서 예를 들었듯이 이 자료구조를 파일에 쓴다고 하면 헤더 부분 따로 그리고 data 영역을 따로 두 번에 걸쳐 파일 쓰기를 해야 한다. 결국 이 말은 이 자료구조에 접근하려면 헤더 한번, 실제 한번, 즉 두 번의 참조가 필요하다는 이야기다.

여기서 쪼잔하게 따지면 위 코드는 데이터 참조의 지역성(locality)이 줄어들어 캐시 미스를 한번 더 유발할 수 있는 단점도 있다. 요즘 프로세서들 캐시가 수 메가 바이트니 이 정도 괜찮다라는 생각을 함부로 하지 말자. 정말 성능이 중요한 프로그램은 이런 사소한 지역성 차이에서 오는 차이를 결코 무시할 수 없다. 비슷한 예로 “구조체의 배열(Array of Structure)” 이냐 “배열의 구조체(Structure of Array)”라는 문제도 있다 (우리말과 영어의 어순이 반대라 이거 이름이 참 헷갈린다). 간단한 예를 보면..

구조체의 배열(Array of Structure, AoS):

struct {
double x;
double y;
double z;
} vector[4];

배열의 구조체(Structure of Array, SoA):

struct {
double x[4];
double y[4];
double z[4];
} vector_set;

각각 상황에 따라 적절히 선택해서 골라야 한다. 사소해 보이지만 경우에 따라 큰 성능 차이가 나타날 수 있다.

AoS 같은 접근은 루프 한 순회에서 모든 원소들이 접근될 때 유리하다. vector[i]을 읽으면 일단 i번 째 x, y, z는 모두 캐시에 올라오기 때문이다. 반면 원소는 3개인데 루프에서 고작 x원소만 접근 된다면? 이 vector 배열이 매우 크다면 손실은 심각하다. 그럴 때는 SoA 구조가 바람직하다. 그 외에도 사소한 영향을 더 생각할 수 있지만 생각보다 글이 길어져서 이쯤에서 그만..

한줄요약: char data[1]; 같은 것이 보여도 쫄지 맙시다.

by object | 2009/03/07 11:12 | 컴퓨터 | 트랙백(3) | 덧글(17)
트랙백 주소 : http://minjang.egloos.com/tb/2254472
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from 그니 at 2010/03/24 16:45

제목 : char data[1]의 역할은?
char data[1]의 역할은?...more

Tracked from 김재호의 디지털보단 아.. at 2011/01/07 12:58

제목 : 하위 디렉터리의 파일이 변경 되었는지 감지하는 법
프로그램을 짜다보면, 특정 디렉토리 내에서 파일 혹은 디렉터리가 변경되었음을 감지해내야 하는 경우가 가끔 생긴다. 윈도우즈에서는 FindFirstChangeNotification과 그 패밀리 함수들을 통해서 이를 쉽게 확인할 수 있다. 감시하고 싶은 디렉터리의 바로 하위 디렉터리 뿐만 아니라, 모든 하위 디렉터리까지 알림을 받을 수 있도록 API가 설계되어져 있다. FindFirstChangeNotification은 파일 변경 알림을 위한 커널......more

Tracked from 조급하지말고 천천히 at 2011/12/15 08:26

제목 : [Redis] sds.c를 분석해보자
sds는 Redis내에서 사용되는 문자열 관련 데이타 입니다. 문자열을 붙이고, 메모리가 부족하면 다시 재할당하고....... 비교하고 삭제하고, 이런건 알겠는데 sdshdr의 buf[]의 개념이 뭔지 너무너무 궁금했습니다. buf[]가 할당된 메모리 내에서 두개의 int(len, free)다음을 가리킨다는것을 가정하고 진행했는데, 그건 개발자가 아니라는 책임감때문에 kldp에 질문을 해서 http://kldp.org/node/128629 fl......more

Commented by daybreaker at 2009/03/07 14:13
옥의 티(?) 발견 : "char data[1]의 역할은"이라고 한 부분의 코드에서 int data_length, int *data가 아니라 int data_length, char *data 아닐까요? (뭐, data_length가 데이터의 원소 개수가 아닌 데이터가 차지하는 실제 바이트 크기라고 하면 괜찮겠지만...)

그나저나 저도 항상 저 끝을 포인터로 두면서 두 번씩 할당해야 하는 것이 불만이었는데 이렇게 하면 비교적 깔끔하게 해결되는군요. 앞으론 이렇게 써야겠습니다. =3=3
Commented by object at 2009/03/08 15:19
네, 말씀대로 int*, char*에 따라 data_length의 의미가 바뀌겠지요.
Commented by 최종욱 at 2009/03/08 13:22
전 C, C++ 프로그래머가 아니라서 char data[1];을 보고 '포인터겠군. 근데 왜 1이 아니지? 1에 다른 의미가 있나?'라는 생각이 자꾸 들어서 혼란에 빠져있다가 내려보고서야, '아, 경고를 피하는 용도였구나' 깨달았습니다. ㅠㅠ 여하튼 재미있는 이야기 감사합니다.
Commented by 궁금... at 2009/03/09 10:03
좋은 글 감사합니다.
궁금한게 있는데요...
char data[1];
이라고 선언을 하면, 1바이트만큼의 메모리가 할당이 될텐데, malloc 없이 memcpy(hd->data, data, data_length); 표현이 가능할지 모르겠네요.

즉, 1바이트 영역에 data_length만큼 복사가되어 언젠가는 segmentation fault 나 메모리 꼬임(?) 현상이 발생하지 않을까요?
Commented by object at 2009/03/09 10:27
malloc을 할 때 헤더와 헤더 뒤에 따를 데이터의 크기를 한꺼번에 할당합니다. 말씀대로 char data[1]을 하면 1바이트만큼 헤더가 더 할당이 되겠죠. 실제로는 alignment로 4바이트가 더 해지겠지만 그렇다치더라도 이미 데이터가 들어갈 공간은 충분히 확보했기 때문에 전혀 세그폴트가 날 수는 없습니다.

data가 지금 char [1]로 선언이 되어있지만 char [2] 부터는 바로 뒤 따르는 데이터가 오게 됩니다. 그런 꼼수를 이용한 것이죠.
Commented by 몽몽이 at 2009/03/09 11:39
오랫만에 들렀습니다.
음... 언젠가 이 방식이 적법한 것이냐에 대한 논쟁을 봤던 기억이 나네요.
개인적으론 별로 선호하지 않는데... 민장님은 어떠신가요?
Commented by object at 2009/03/09 12:23
저도 이런 걸 제가 직접 써본적은 없습니다 :) 그런데 코드가 모호해진다는 단점 외에는 장점이 더 많다고 봅니다.
Commented by 궁금... at 2009/03/09 13:25
malloc(sizeof(HEADER) + data_length); 에서 뒷부분을 놓쳤군요...

제가 자세히 보지 못한것 같습니다.ㅠㅠ
Commented by felucca at 2009/03/10 03:18
C syntax가 못나서...
Commented by kalstein at 2009/03/10 09:42
아...좋은 스킬(트릭? ^^;;) 하나 배워가네요.

전 직관적(?)인 방법만 알고있었는데...저렇게 해도 깔끔하니 좋겠네요.
Commented by dawnsea at 2009/03/10 09:43
원시적인 네트웍 프로토콜 래핑 래핑 할때 종종 data[0] 으로 썼는데요..
안 되는 컴파일러도 종종 만납니다;;;
예) ARM용 SDT -_-;;



Commented by purnnamu at 2009/03/29 09:08
코드에 코멘트를 적어 보통 사람이 이해하기 쉽게 만들기만 한다면, 성능을 향상시키는 좋은 방법중의 하나가 되겠네요. 프로그램 할때 항상 캐쉬를 고려해야 한다는 점에는 100% 동의합니다. 이런 방법을 널리 공유해 주셔서 감사하게 생각합니다.
Commented by 마루나래 at 2010/08/27 00:05
구조체에 data[1] 이 쓰인 소스가 있어 뭔가 헤맸는데 여기서 답을 찾아가네요~ 좋은 글 감사합니다
Commented by damduc at 2011/01/27 10:02
저도 소스 보던 중에 궁금하던 했는데 딱 해결 해주셨네요.. ^^
감사합니다~~!!
Commented by sail2 at 2011/02/11 16:07
지나던 길에 들릅니다. 약간 착오가 있는 것 같아서 드리는 말씀인데요,
flexible array는 C 규격에서 크기가 없는 array로 선언하도록 되어있습니다.
위에서 언급하신것처럼 크기를 [0] 으로 잡는 코드는 예전 규격으로 정립되기 이전에 편법으로 쓰이던 방법이고 컴파일러에서 waring이 나던것도 사실이라 규격으로 인정 된 후 [] 로 잡아서 하도록 되어있습니다.

크기를 [1]로 잡게되면 크게 문제는 없으나 sizeof (구조체) 했을 때 flexible array의 크기까지 합산되기 때문에 나중에 pointer 연산시 offset을 구할 때 항상 고려를 해줘야하는 불편함이 있습니다.
리눅스 커널 소스에 보면 위와 같은 방식으로 연산하는 부분이 있습니다. 그때 문제가 되겠지요.
Commented by sail2 at 2011/02/11 16:09
struct A {
int data1;
int data2;
char data[];
};
에서 sizeof를 구해보면 크기가 8로 나옵니다. (flexible array는 무시하기 때문)
그러나
struct A {
int data1;
int data2;
char data[1];
};
을 sizeof 해보면 크기가 12가 나옵니다. (char data[1]의 크기가 추가. 구조체 메모리 할당 메커니즘 때문 4가 증가)

이런 차이가 있습니다.

캐쉬에 관련된 내용은 정말 도움이 많이 되었습니다.
이상 저의 짧은 소견이었습니다. 감사합니다.
Commented by GS at 2011/02/15 22:05
HEADER* hd = (HEADER*)malloc(sizeof(HEADER));
hd->size = 1024;
hd->type = 0;
hd->data = (char*)malloc(hd.size);

마지막 줄에 hd.size 는 오타이신가요..?
하여튼, 저는 왜 이걸 원래 방법 처럼 구현하지 않으신지 궁금합니다.
그러니까.. char* 방법으로도, 한번의 malloc 으로 똑같이 사용하면 무슨 문제가 생기는지요.

:         :

:

비공개 덧글

<< 이전 페이지 다음 페이지 >>





by 김민장 2008 이글루스 TOP 100
최근 등록된 덧글
개발자 입장에서의 수많은 ..
by Jiyoon at 02/04
저도 아들 돌잔치때 돌잡이 ..
by 박상욱 at 01/18
미국 대학원 원서 작성중에 p..
by 태클사이야 at 01/13
TO: 박PD 로그인 하지 않아..
by 박응용 at 01/10
http://gigglehd.com/zbx..
by dhunter at 12/28
우와.. 좋네요. 태반이 ..
by 윤광배 at 12/17
항상 좋은 글 잘 보고 있습니..
by y2k at 11/23
글이 좋아서 제 블로그에 담..
by 쏭섭 at 11/23
최근 등록된 트랙백
조엘 스폴스키의 강연 (Sta..
by 인덕원칸타타
[Redis] sds.c를 분..
by 조급하지말고 천천히
메뉴릿
이글루 파인더

website counter

Add to Google

rss

skin by 이글루스