컴퓨터활용/티맥스

구조체 패딩(padding)문제

멜번초이 2008. 4. 21. 21:11
반응형

1. 구조체와 패딩비트

아래와 같은 구조체를 선언했다고 하자.
struct test_s
{
  char a;
  int b;
} test;

char가 1바이트이고 int가 4바이트인 시스템에서 위의 구조체를 선언하고 sizeof()로 구조체의 사이즈를 찍어보면 얼마가 나올까? 생각대로라면 5바이트가 나와야 한다. 1 + 4 = 5 이니까..

그런데 대부분의 컴파일러에서 실제로는 8바이트가 나온다. 이유는 패딩비트가 추가되어서 그렇다. 몇몇 컴파일러는 구조체의 필드를 메모리에 위치시킬때 중간에 빈 공간없이 쭉 이어서 할당하는 경우도 있지만, 대부분의 컴파일러는 성능향상을 위해 CPU가 접근하기 쉬운 위치에 필드를 배치한다. 그러다보니 중간에 빈 공간이 들어가게 되는것이다. 이 빈 공간이 바로 패딩비트이다.

이에 대해서 좀 더 자세히 알아보자.

32비트 CPU는 메모리에서 값을 읽어올때 한번에 4바이트(32비트), 64비트 CPU는 한번에 8바이트(64비트)를 읽어온다.

32비트 CPU를 가진 시스템에서 CPU가 메모리상에 정의된 위 구조체의 a 멤버로 접근하려면 어떻게 해야할까? 간단하다. 구조체 test의 시작번지에서 32비트를 읽어와서 그 중 맨 앞 8비트만 사용하면 된다.

그럼 이제 그 다음 멤버인 b에 접근하려면 어떻게 해야할까? 이건 좀 복잡해진다. 구조체 test의 시작번지에서 32비트를 읽어와도 멤버 b의 비트에 모두 접근 할 수 없다. 그래서 두번 메모리를 읽어서(총 64비트), 첫번째 읽은 값에서 뒤의 24비트와 두번째 읽은 값에서 앞의 8비트를 합쳐서 멤버 b의 값을 구할 수 있다. 이렇게하면 한번에 할일을 두번에 걸쳐서 하는것이기 때문에 당연히 성능저하가 발생한다.

그래서 대부분의 컴파일러는 CPU가 접근하기 쉬운 메모리 위치에 필드를 배치시키기 때문에 아래와 같이 패딩비트가 자동으로 들어가게 된다.

char a: 1byte
padding bit: 3bytes
int b: 4bytes

위와 같이 패딩비트가 들어가서 총 8바이트가 되면 CPU가 각 멤버에 접근할때 한번씩만 메모리를 읽으면 각 멤버의 값을 구할 수 있다. 쓸모없는 메모리를 3바이트나 낭비하는 꼴이 되어버리지만 CPU가 각 멤버에 접근할때 한번씩만 메모리를 읽으면 되기 때문에 성능저하가 발생하지 않는다.


2. 네트웍을 통한 구조체 전송

구조체를 사용할때 대부분의 경우에는 위와 같은 복잡한 패딩비트에 대해 신경쓸 필요는 없다. 가끔 구조체의 전체 사이즈가 프로그래머가 생각했던것과 다르게 나오는게 문제가 되는 경우도 있겠지만..

그런데 네트웍을 통해서 구조체 자체를 전송하려고 하면 패팅비트가 굉장히 중요한 변수가 된다. 왜냐하면 구조체가 메모리에 정의되는 형태는 OS와 컴파일러에 따라 달라지기 때문이다. 동일한 구조체를 서로 다르게 메모리에 정의하고 있는 시스템끼리 메모리에 있는 구조체 내용을 그대로 주고 받는다면 구조체의 각 멤버는 서로 다른값을 가지게 된다.

패딩비트는 삽입되는 위치가 컴파일러에 따라 달라진다. 또한 32비트와 64비트 시스템은 동일한 구조체에 대해서 삽입하는 패딩비트의 수가 각각 다르다.

그럼 시스템 A(32비트)가 시스템 B(64비트)에게 아래의 구조체를 네트웍으로 전송하고, 시스템 B는 받은 패킷을 아래 구조체로 캐스팅해서 그대로 사용하는 경우 어떤 문제가 생길까?

* 참고로 char = 1byte, long long = 8bytes
struct test_s
{
  char a;
  long long b;
} test;

32비트 시스템에서는 위 구조체를 사용할때 멤버 a 뒤에 3바이트의 패딩비트를 넣어서 구조체 사이즈가 12바이트가 된다. 반면에 64비트 시스템에서는 7바이트의 패딩비트를 넣어서 구조체 사이즈가 16바이트가 된다.

따라서 구조체의 시작위치에 있는 첫번째 멤버 a는 값이 변하지 않지만, 그 다음 멤버들은 값이 다 바뀌게 된다.

그럼 어떻게 하면 구조체를 네트웍으로 안전하게 전송 할 수 있을까? 여기엔 두가지 방법이 있다.

첫째, #pragma 또는 #packed 키워드를 사용해서 컴파일러가 패딩비트를 사용하지 않도록 하는 방법이 있다. 하지만 이 방법은 C 표준이 아닌 관계로 이식성이 없다.

#pragma pack(1)
struct test_s
{
  char a;
  int c;
} test;
#pragma pack(8)
구조체 선언할 때 1 byte 단위로 pack 시킨다고 지정해 주고 선언이 끝난 후에 적절히 4 byte나 8 byte 로 (원래대로) 확장(원복) 시켜 주는 방법이다.

둘째, 프로그래머가 패딩비트를 수동으로 관리해주면 된다. 즉, 아래와 같이 구조체를 만들때 dummy 값을 패딩비트로 넣으면 된다.

struct test_s
{
  char a;
  char b[3];
  int c;
} test;

위에서 멤버 b가 패딩비트이다. 위 구조체는 4의 배수인 8바이트의 사이즈를 가진다. 그러나 몇 바이트를 기준으로 정렬할지는 순전히 OS와 컴파일러에 따라 달라진다. 따라서 네트웍으로 구조체를 직접 보내는 방법은 올바른 방법이 아닌것으로 생각된다.

그럼 하나 더 생각을 해보자. 무조건 4의 배수로만 구조체의 크기를 맞춰주면 문제가 해결이 될까? 
int(4바이트)를 기준으로 맞추면 대부분의 경우 정확하다. long long 변수가 있는 경우는 8 바이트를 기준으로 맞춘다. 그러나 OS와 컴파일러에 따라서 약간의 특성을 타기 때문에 항상 보장된다고는 볼 수 없다.
 
struct test_s
{
  char a;
  char b[7];
  long long c;
} test;

하지만 아직 바이트오더 문제가 남아있다. 이는 아래 문서를 참조하면 된다.
http://superkkt.com/138


- 부록 A

ANSI C 기반의 프레임웍인 Glib에 G_MEM_ALIGN 이라는 메크로가 있다. 이 메크로는 시스템의 메모리 정렬 값을 알려주는데 이에 대한 메뉴얼 페이지의 설명은 아래와 같다.

Indicates the number of bytes to which memory will be aligned on the current platform.

그리고 G_MEM_ALIGN의 선언은 아래와 같이 되어있다.
#if GLIB_SIZEOF_VOID_P > GLIB_SIZEOF_LONG
#  define G_MEM_ALIGN   GLIB_SIZEOF_VOID_P
#else   /* GLIB_SIZEOF_VOID_P <= GLIB_SIZEOF_LONG */
#  define G_MEM_ALIGN   GLIB_SIZEOF_LONG
#endif  /* GLIB_SIZEOF_VOID_P <= GLIB_SIZEOF_LONG */

음.. 이렇게 간단하게 시스템의 메모리 정렬 값을 알 수 있는건가?? 단지 void * 와 long 중 큰 값이 메모리 정렬 값인가??
--> 2008년 6월 10일 추가: CPU의 비트수가 메모리 정렬값이라고 가정하고 만든 코드인것 같다. 가정이 아니라 그게 정답인가?? 아무튼 32비트 시스템에서는 long 타입이 32비트이고, 64비트 시스템에서는 long 타입이 64비트이기 때문에 이렇게 코드를 만들었을 것으로 생각된다.


- 추가 (2007년 8월 28일)

위에 언급했던 패딩비트를 프로그래머가 직접 관리하는 방법을 실제 프로그래밍에 적용해본 결과, 동일한 플랫폼 사이에서는 사용이 가능하지만 이기종 또는 서로 다른 프로그래밍 언어 사이에서는 full portability를 기대할수 없는것 같다. 따라서 가장 안전한 방법인
직렬화 과정(serialization)을 거치는것이 좋다. 그리고 TPL이라는 serialization utility가 있다. TPL은 라이브러리 형태로 사용되는것이 아니라 하나의 소스 파일로 이루어져 있어서 그 파일을 소스 트리에 추가해서 사용하면 된다. 참고로 BSD 라이센스를 가지고 있다.


<원본글 출처 : http://superkkt.com/159>



구조체 패딩 문제의 다른 글


 struct friends {
   char name[15];
   char tel[15];
   int age;
};


위 구조체 크기는, 눈으로 계산해보면, 15+15+4 이므로, 총 34 바이트가 될거같죠.
하지만, sizeof(struct friends)해서, 값을 구해 찍어보면, 36이 나옵니다.


왜냐면, 32비트 CPU는 메모리번지 계산을할때 효율적인 성능을위해
4바이트 단위(정수단위)로 이용합니다.


그래서, 첫 멤버인 name에대한 메모리를 할당할때 15바이트를 할당한게 아니라,
정수형을 이용하여, 전체를 담을 수 있는 최소 단위로 할당하죠. 그래서 정수형
4개를 할당하면, 16바이트가 되어, name[15]를 담을 수 있죠. 그러면, 남는
바이트는 여기서 1바이트가 되는데 이것을 패딩(Padding)이라고 합니다. 옷에서
어깨에 넣는 뽕을 패딩이라고 하든가요? 또 겨울 파카에도 패드로 채워졌다고 하고요.
이렇듯, 빈곳을 걍 채우는것을 패딩(padding)이라고 합니다. 


아래 tel도 마찬가지 이유로 16바이트가 할당됩니다.
int age는, 원래 정수형이니 상관이 없겠죠.


이것을 해결하는 방법은,


[첫번째] name, tel구조체 멤버갯수를 name[16], tel[16]; 처럼 생성시 4의
배수에 맞춰주는 방법이 있읍니다.


[두번째] #pragma pack(1) 이라고, 소스 상단에 써주면 1바이트 단위로
잘라 넣게되 위의 문제가 해결되죠. 그러면, 의문이 들죠. 예초 저렇게 하게
만들든가. 하지만 이렇게 하면 성능이 많이 떨어집니다.


[세번째] fwrite등의 구조체전체를 저장해야할때, 멤버를 일일이 써주세요.
먼저, name[15]를 sizeof 계산해서 써주고, 다음 sizeof(tel)만큼 써주고요


첫번째 방법은 절대 권장사항은 아니고, 두번째나 세번째를 이용하세요.


출처 : 네이버


또 다른 지식정보


구조체를 연달아 선언했음에도 시작주소가 차이나는 것은 구조체 정렬(Alignment)을 하기 때문입니다.

이는 컴퓨터에서 메모리를 읽고/쓰는 물리적인 성능을 끌어올리기 위해서 의도적으로 저장되는 위치를 저장하는 것입니다.

CPU는 캐시를 사용하는데, 캐시가 저장된 메모리를 읽어올때



| 0000000000000000 |
| 0000000000000000 |
| 0000000000000000 |
      :


이런식으로 있는 메모리 구조에서 구조체 A는 int, char, double 의 자료를 가져 13바이트 크기를 가진다고 가정한다면

각각의 자료형마다 정해진 위치대로 구조체의 멤버위치를 조정하게 됩니다.


| 0000000000000000 |


이렇게 비트를 붙여서 저장할 경우, 캐시는 char 형의 자료를 읽기 위해 메모리를 나누어서 읽어들여야 할 것이고, double 형을 읽기 위해서도 5바이트 부터 13바이트까지 나누어서 하나씩 검색하고, 읽어들여야 할 것입니다.

이는 매우 비효율적인 방법이기 때문에, 속도를 향상시키기 위해서 (메모리를 조금 낭비하기는 하지만) 캐시가 읽어들이기 쉽도록 구분을 두어서 저장하는 것입니다.


| 0000000000000000 |


이런식으로 말이죠.

각 타입마다 정해진 크기가 있습니다. int형은 4바이트, double형은 8바이트와 같은 형태이며 int형을 찾으려면 캐시는 메모리를 4바이트 단위로 읽고, double형을 찾으려면 8바이트 단위로 읽으면 되므로 메모리를 읽어들이는 횟수가 줄어서 속도가 향상되게 됩니다.


그리고 이때 사용하지 않아서 버려지게 되는 메모리를 패딩(Padding)이라고 합니다.


또한 구조체의 크기가 그렇게 크지 않다 하더라도 캐시가 구조체를 빨리 읽어들일수 있도록, 컴퓨터는 캐시의 배수가 되는 위치에 구조체를 위치시키게 됩니다.


소스에서 구조체의 시작주소가 16바이트 차이나는 것은, CPU가 사용하는 캐시의 크기가 16바이트 이기 때문입니다.


구조체는 일반 타입과 달리 복합적인 타입의 집합이므로, 컴퓨터에서 속도를 증가시키기 위해 일부러 그렇게 조정한 것이라고 할 수 있습니다.

출처 : http://blog.naver.com/kkan22/80062807128

반응형