비트필드의 크기(Size of Bit-fields)

union U_Data
{
    unsigned int _key;
    struct S_Data
    {
        unsigned short _code;
        unsigned short _no;
    } _evtkey;
};

개발자는 두 값(code, no)를 조합해서 고유한 키값을 손쉽게 만들기 위해 U_Data를 아래와 같이 사용하고 있다.

U_Data data;
data._evtkey._code = 10;
data._evtkey._no = 4;
unsigned int another_key = data._key; // 고유한 키값을 손쉽게(?) 얻었다!

그런데, _code의 크기가 100,000을 넘어가게 생겼다. 그래서 타입을 int로 바꿔야 했다. 이미 _key는 unsigned int 타입으로 여러 함수의 인자로 사용되고 있기 때문에, U_Data의 현 크기(4바이트)를 바꾸기는 쉽지 않다.

다행히 _no의 범위는 기껏해야 수십을 넘어가지 않았으므로, 비트필드를 사용하여, _code에 24비트를, _no에 8비트를 사용하기로 하였다.

union U_Data
{
    unsigned int _key;
    struct S_Data
    {
        int _code:24; // 타입을 바꾸고, 24비트를 할당
        unsigned short _no:8; // 8비트를 할당
    } _evtkey;
};

다른 코드는 손대지 않고, 손쉽게 _code의 크기를 늘렸다. 근데, 제대로 동작하지 않는다. 앞선 예제와 반대로, _key를 이용해서 _code와 _no를 구하려고 하니, _no에는 쓰레기 값이 있다.

이유는, 1바이트 패킹 시(#pragma pack(1)), VC++에서, U_Data의 크기를 4바이트가 아닌, 6바이트로 다루기 때문이며, 비트필드가 어떻게 메모리에 할당되는지에 대해서는 표준문서가 명시하고 있지 않다.

Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined.

– ISO/IEC 14882:2011 9.6.1

VC++에서는 비트필드가 명시된 타입에 순차적으로 값을 구겨 넣으며, 타입이 변경되면 새로운 타입에 새롭게 할당을 시작한다. 즉, S_Data의 첫번째 데이터 타입이 int이므로 4바이트에 24비트를 할당하고, 그 다음 2바이트인 unsigned short타입에 8비트를 할당해서, 총 6바이트가 사용된다. int타입인 _code에 8비트가 남았다고 해서 _no를 int에 할당하는 것이 아니란 이야기다(gcc는 4바이트를 할당한다). 명시적으로 새로운 할당을 시작하려면 비트의 크기에 0을 넣으면 된단다(http://msdn.microsoft.com/en-us/library/ewwyfdbe.aspx).

이 문제는 _code와 _no의 타입을 int로 맞춰주는 것으로 해결된다.

문제를 해결하고, 기억을 되살려보니, 정확히 2005년 10월 4일에 이와 똑같은 문제를 겪고 남겨둔 기록이 있었다.


struct T1
{
    int m_a : 5;
    int m_b : 6;
    int m_c : 5;
};
struct T2
{
    short m_a : 5;
    short m_b : 6;
    short m_c : 5;
};
struct T3
{
    char m_a : 5;
    char m_b : 6;
    char m_c : 5;
};

1로 패킹했을 경우(#pramgma pack(1)), 각 구조체의 크기는 각 각 얼마일까?

만약, sizeof()의 결과로 2를 기대했다면, T1과 T3는 잘못된 결과를 야기한다.

T1의 경우, sizeof()의 결과는 4이다. 총 비트의 합은 16이지만, 첫 번째 멤버변수의 크기가 4이기 때문에 컴파일러는 모든 비트를 첫 번째 변수의 크기인 4바이트에 구겨 넣는다. 나머지 16비트는 사용되지 않는다.

T2의 경우는 2이다. 16비트는 short int형에 딱 맞게 들어간다. 따라서 낭비하는 비트는 없다.

T3의 경우는 3이다. 컴파일러는 char의 크기인 1바이트에 5비트를 우선 구겨 넣는다. 그 다음 6비트를 확보해야 하는데, 첫 번째 1바이트에는 3비트밖에 남은 공간이 없으므로 또 하나의 char형 크기인 1바이트에 6비트를 확보한다. 마찬가지로 그 다음 5비트는 적절한 공간이 없으므로 새로운 char형 크기인 1바이트를 또 할당한다. 그래서 총 3바이트 크기가 되며, 8비트를 사용하지 않은 채 남겨두게 된다.

대부분의 컴파일러는 이와 같은 방식으로 동작한다. 그러나 표준문서에 비트필드의 크기에 대한 언급은 없기 때문에 엄밀히 따지면 이는 컴파일러의 구현에 따라서 틀려질 수 있다. 참고로, 비트필드에 접근하는 속도는 표준타입의 멤버변수에 접근하는 시간보다 느리기 때문에, 속도를 위해서라면 바람직한 방법은 아니다.

Leave a Reply