구조체 패딩 비트에 대해서. struct padding bit

by Blogger 하얀쿠아
2011. 8. 20. 21:13 소프트웨어 Note/C++ MFC

아래와 같은 구조체를 선언했다고 하자.


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

 = 8byte

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

 

하지만 패딩 비트에도 문제점이 발생한다.

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

 

그런데 네트워크를 통해서 구조체 자체를 전송하려고 하면 패팅 비트가 굉장히 중요한 변수가 된다. 왜냐하면 구조체가 메모리에 정의되는 형태는 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

이 댓글을 비밀 댓글로

[C/C++]유용한 #pragma directive

by Blogger 하얀쿠아
2011. 5. 21. 09:51 소프트웨어 Note/C++ MFC
출처 : http://eslife.tistory.com/187

※ 주의 : 아래에서 기술하는 내용은 Visual C++ 컴파일러에서만 확인된 내용입니다.


지난번 #define 팁 에 이어 이번에는 필수는 아니지만 사용할 경우 아주 편리한 #pragma 지시자를 간단하게 소개하려고 합니다.


1. 헤더 파일을 한번만 읽어 들이기


아마도 이 경우가 #pragma를 가장 널리 사용하게 된 이유 중에 하나라고 생각되는데요

저도 언제나 헤더 파일을 새로 코딩 할 때 이 한 줄을 먼저 적게 됩니다. (아주 짧고 멋집니다)

#pragma once

#pragma once 가 없을 당시에는 아래와 같이 헤더 파일 내용을 #ifndef ~  #endif 문장을 사용해서 복잡하게 써주어야 했습니다. (요 복잡한 게 한 줄로 줄었으니 사용하지 않을 수 없죠^^)

#ifndef _MY_HEADER_FILE_H_

#define _MY_HEADER_FILE_H_

// 실제헤더파일내용

 

#endif

하지만 이러한 방식은 헤더 파일마다 새로운 이름의 #define 문을 작성해야 했고 실수로 #endif 를 빼먹거나, #ifndef 과 짝을 잘못 이룬 경우 코딩과 상관없이 재앙을 불러오기 일쑤였습니다.

Visual Studio .NET 2003버전부터였던 것으로 기억되는데 프로젝트 위저드가 자동으로 만들어내는 헤더 파일에 #pragma once 가 들어 있어 그때부터 제가 만드는 헤더 파일도 이 방법을 애용하게 되었습니다.

 

2. Structure Alignment 조정하기


클라이언트 프로그램을 하다 보면 UNIX 나 기타 서버와 소켓연결을 통해 데이터를 주고 받는 경우가 많습니다. 서버와 통신을 하다 보면, 보통 UNIX 로부터 전송되는 구조체와 클라이언트 프로그램의 구조체의 Alignment를 맞춰주어야 하는 경우가 생깁니다.

#pragma 를 사용해서 구조체 Align 을 조정할 수 있다는 것을 몰랐던 시절에는 하는 수 없이 전체 프로젝트의 Align 속성을 ‘1’ 바이트로 변경하곤 했습니다.

사용자 삽입 이미지

이렇게 할 경우

-       Structure Alignment 를 기본인 8바이트로 사용할 경우보다 약간의 메모리 접근 성능이 저하되고

-       3’rd Party Library 와 구조체를 주고 받을 때 양쪽이 Alignment 가 틀려 고생할 가능성이 생기고

-       프로젝트를 신규 생성할 때마다 Alignment 값을 변경하지 않으면 이 역시 코딩과는 상관없이 서버로부터 받은 데이터가 몽땅 깨지는 현상을 만나는 문제가 있었습니다.

그래서 가급적이면 꼭 필요한 구조체에 대해서만(이 경우 서버와 통신하는 구조체) Structure Alignment 를 조정하는 것이 좋습니다.

#pragma pack 을 사용하면 특정 구조체만 간단하게 Alignment 속성을 바꿀 수 있습니다.

#pragma pack(push, 1)

struct ABC

{

           int BaseOption1;

           int BaseOption2;

           int BaseOption3;

           char szText[100];

           BYTE szAddress[100];

};

#pragma pack(pop)

#pragma pack 의 괄호 안에 들어가는 숫자 ‘1’ 1바이트로 Alignment 를 하라는 지시이고, #pragma pack(pop)은 이전 상태(디폴트가 8 바이트 Alignment 이면 8 바이트)로 돌아가라는 지시어입니다.

알고 보면 너무 간단한데 몇 년 동안 이 방법을 몰라 고생했더랬습니다.

 

3. 귀찮고 처치 곤란한 경고 메시지 없애기


좋은 프로그램을 작성하는 개발자라면 늘 Warning Level 을 최고로 올려 빌드 하실 텐데요

이 경우 의미 없는 경고도 어쩔 수 없이 많이 만나게 됩니다.

특히 .NET 2005 에서는 버그인지 모르겠지만, 주석문에 들어 있는 한글을 만나면 희한한 경고를 만나게 되는 경우가 많습니다.

warning C4819: The file contains a character that cannot be represented in the current code page (949). Save the file in Unicode format to prevent data loss

가능한 모든 경고는 없애야겠지만, 어쩔 수 없이 무시하고 넘겨야 하는 경고들도 있는데 이런 경고들은 다음 번 빌드부터는 보지 않도록 아래와 같이 지정할 수 있습니다.

#pragma warning (disable:4819)

위 내용은 4819 오류 경고 표시를 하지 말라는 뜻으로 쓰입니다. 이 경우도 push pop 을 사용해서 특정 소스에서만 경고가 안 나오게 설정할 수도 있습니다.

 

4. 라이브러리 링크하기

사용자 삽입 이미지

일반적으로 라이브러리를 링크하는 방법은 프로젝트 Linker 옵션에서 사용할 프로젝트 라이브러리를 지정해 주는 방식을 많이 사용합니다.

#pragma 를 사용하면, 프로그램 소스에서 직접 다른 라이브러리를 링크할 수 있습니다.

#pragma comment( lib, "../TEST_DLL/release/Testdll.Lib" )

이 방식은 프로젝트 속성창 > 링크옵션 > Testdll.lib 를 추가하는 것과 동일한 방식으로 라이브러리를 링크합니다.

프로젝트 속성을 통해 링크를 지정하는 방식과, #pragma 를 통해 링크를 지정하는 것 중 저희는 후자를 선호합니다.

후자와 같이 프로그램 소스에 링크 정보가 있을 경우 특정 모듈에 대한 종속정보를 단순히 소스 검색을 통해서 확인할 수 있고 프로젝트가 변경, 확장, 제거 될 때 마다 매번 프로젝트 속성을 재차 확인해야 하는 번거로움도 없어 자주 사용하게 되더군요

 

그외에도 MSDN에서 #pragma 를 찾아 보면 상당히 많은 지시자가 있는데 혹 공유할만한 내용 있으시면 많은 지적 바랍니다

Tags
이 댓글을 비밀 댓글로