출처 : http://superkkt.com/159

 

아래와 같은 구조체를 선언했다고 하자.
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 표준이 아닌 관계로 이식성이 없다.

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

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


위에서 멤버 b가 패딩비트이다. 위 구조체는 4의 배수인 8바이트의 사이즈를 가진다. 이제 이 구조체는 어떤 시스템으로 전송을 하더라도 문제 없이 사용 할 수 있다.
--> 2007/2/8 추가 - 위 문장이 항상 참이 되는것은 아닌것 같다. C99 표준에서도 아래와 같이 구조체의 정렬은 구현체에 따라 달라진다고 되어있다.

Each non-bit-field member of a structure or union object is aligned in an implementationdefined manner appropriate to its type.

그렇다면 몇 바이트를 기준으로 정렬할지는 순전히 OS와 컴파일러에 따라 달라진다는 얘기이다. 따라서 네트웍으로 구조체를 직접 보내는 방법은 올바른 방법이 아닌것으로 생각된다.

그럼 하나 더 생각을 해보자. 무조건 4의 배수로만 구조체의 크기를 맞춰주면 문제가 해결이 될까?

그건 아니다. 정확히 말하자면 "구조체 내부에서 사용된 필드의 타입 중 가장 큰 값의 배수로 맞춰야 한다."가 정답이다. 위의 구조체의 경우에는 가장 큰 필드가 int(4바이트)이기 때문에 4의 배수로 정렬을 해주면 되고, 그 위의 구조체의 경우에는 가장 큰 필드가 long long(8바이트)이기 때문에 아래와 같이 8의 배수로 정렬을 해줘야 한다.
--> 2008년 6월 10일 추가: 근거가 불분명하고 위에서 설명한바와 같이 구조체의 정렬은 구현체에 따라 다르기 때문에 이런 가정을 하고 프로그래밍 하는것은 상당히 위험하다.

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 라이센스를 가지고 있다.

안정적인 DNS서비스 DNSEver DNS server, DNS service
Posted by 키르히

댓글을 달아 주세요