C언어) 비트의 세계 - 패킹 & 언패킹

Lapis0875·2022년 10월 24일
0

c언어

목록 보기
13/21
post-thumbnail

😎비트수준으로 접근해보기

이번 편은 비트 수준으로 데이터를 다뤄보는 이야기에요. 2부작의 마지막 편이니, 끝까지 힘내봐요! 😉

📦패킹과 언패킹?

패킹과 언패킹이라니, 언뜻 들어보면 생소할 수 있는 용어들이에요. 박스 포장하고 푸는건가? 같은 생각이 든다면, 정답이에요! 다만 C언어에서는 데이터를 포장하고 풀 뿐이에요.

  • 패킹 : 여러 정보를 적은 바이트로 압축하는 것
    • 메모리를 절약할 수 있어요.
    • 데이터 송수신에 시간이 덜 걸려요.
  • 언패킹 : 패킹된 정보를 사용하기 위해 정보를 추출하는 것

패킹

간단한 사례를 들어 패킹과 언패킹을 직접 다뤄볼게요.

typedef enum position {WEB, ANDROID, IOS, SERVER} Position;

typedef struct developer
{
	unsigned id;		// 직원 id에요. 4자리의 10진수에요 (0001~9999).
    Position pos;	// 업무 분야에요. position 열거형의 값을 가져요.
    char gender;	// 직원 성별이에요. 'M' 또는 'F'의 값을 가져요.
} Developer;

위 예시의, "developer"라는 구조체를 패킹하고자 해요. 각 멤버별로 몇자리 비트에 저장해야하는지 계산해볼게요.

멤버비트 자리 수설명
.id14최대값인 9999가 00100111 00001111 이니, 14자리 비트가 필요해요.
.pos2position 열거형은 0~3의 값을 가지니, 2자리 비트가 필요해요.
.gender1char형 값이지만, 실제로는 M이나 F의 2가지 값을 가지므로 1자리 비트로 표현할 수 있어요.
---------
전체17전체 17비트니, unsigned 형으로 표현할 수 있어요.

위 구조체를 패킹하는 함수를 만들어 볼게요.

unsigned pack_developer(Developer *data)
{
	unsigned packed = 0;
    packed |= data->id;
    packed |= data->pos << 14;	// id가 14자리 비트이기 때문에, 14자리 비트만큼의 공간을 보존해요.
    packed |= ( ( data->gender == 'M' ) ? 0 : 1 ) << 16;	// id와 pos까지의 공간을 보존하려면 총 16자리가 필요하므로, 성별은 16자리 비트 앞에 저장해요.
    return packed;
}

아래와 같은 코드를 만들어서 확인해봤어요.

void print_packed(unsigned);		// 패킹된 비트열을 확인하기 위한 함수에요.
void print_developer(Developer*);	// 구조체 멤버 값을 확인하기 위한 함수에요.
unsigned pack_developer(Developer*);	// 구조체를 패킹하는 함수에요.

int main(void)
{
    Developer devs[] = {
        [0] = {.id = 1111, .pos = WEB, .gender = 'M'},
        [1] = {.id = 5555, .pos = IOS, .gender = 'F'},
        [2] = {.id = 9999, .pos = SERVER, .gender = 'F'}
    };

    for (int i = 0; i < 3; i++)
    {
        print_developer(&devs[i]);
        unsigned packed = pack_developer(&devs[i]);
        printf("패킹된 값 : %u\n", packed);
        print_packed(packed);
    }
    return 0;
}

void print_packed(unsigned a)
{
    unsigned mask = 1 << 16;		// n개 비트 중 가장 최상위 비트만 1인 마스크에요.
    
    // 2^(n-1) 자리의 비트부터 2^0까지 n개 비트를 반복해요
    for (int i = 1; i <= 17; i++)
    {
    	putchar((a & mask) ? '1' : '0');
        
        // 이진수로 출력할 숫자값을 왼쪽으로 한칸씩 밀어요.
        // 마스크 값이 n개 비트 중 가장 최상위 비트에만 있기 때문이에요.
        a <<= 1;
        if (i == 1 || i == 3)
        	putchar(' ');	// 가독성을 위해, 8개 비트마다 끊어서 표기해요.
    }
    putchar('\n');      // 개행문자를 마지막에 붙여줘요.
}

void print_developer(Developer *data)
{
    printf("id : %d\n", data->id);
    printf("직군 : %d\n", data->pos);
    printf("성별 : %c\n", data->gender);
}

🖥️실행 결과에요.

id : 1111
직군 : 0
성별 : M
패킹된 값 : 1111
0 00 00010001010111
id : 5555
직군 : 2
성별 : F
패킹된 값 : 103859
1 10 01010110110011
id : 9999
직군 : 3
성별 : F
패킹된 값 : 124687
1 11 10011100001111

이제 패킹된 결과를 이해해 볼게요.

id : 1111
직군 : 0
성별 : M
패킹된 값 : 1111
0 00 00010001010111

앞서, pack_developer 함수에서 구조체를 패킹할 때 비트열을 다음과 같이 배치했어요.

1214
.gender.pos.id

이 순서대로 패킹된 비트열을 잘라서 이해해볼게요.
0 00 00010001010111

  • 0 : 성별이 'M'이에요. 'M'일땐 0, 'F'일땐 1로 정의했기 때문이에요.
  • 00 : 직군이 WEB인데, 열거형의 첫 원소이므로 0이에요.
  • 00010001010111 : id에 해당하는 부분이에요. 1111101111_{10}을 2진수로 표현해보면, 00000100 01010111이므로, 같은 값임을 확인할 수 있어요.

언패킹

패킹된 정보는 사용하기 전에 다시 언패킹 해야 해요. 이전에 배웠던 마스킹 연산을 사용해, 패킹된 비트열에서 구조체의 각 멤버에 해당하는 부분의 비트열만 잘라 읽어야 해요.

앞선 예제에서, developer 구조체는 아래와 같이 패킹되었어요.

1214
.gender.pos.id

그렇다면, 이 경우 필요한 마스크는 아래와 같아요.

멤버마스크
.gender1개의 1
.pos2개의 1
.id14개의 1

이제, 마스크 값을 바탕으로 언패킹 함수를 만들어볼게요.

void unpack_developer(unsigned packed, Developer *data)
{
    unsigned GENDER_MASK = 0x1;     // 0b1
    unsigned POS_MASK = 0x3;        // 0b11
    unsigned ID_MASK = 0x3fff;      // 0b11111111111111

    data->id = packed & ID_MASK;
    data->pos = (packed >> 14) & POS_MASK;
    data->gender = ((packed >> 16) & GENDER_MASK) ? 'F' : 'M';
}

각 멤버별 마스크 값을 사용해 패킹된 비트열에서 값을 추출하고 있어요.
pos, gender에서 오른쪽 이동 연산자를 사용하는 이유는 이전 멤버들의 값을 빼고 마스크를 씌우기 위해서에요.

이제 패킹 후 다시 언패킹해 올바르게 언패킹되었는지 확인하는 코드를 작성해볼게요. 이전 패킹 예제의 main 부분만 수정하면 돼요.

int main(void)
{
    Developer devs[] = {
        [0] = {.id = 1111, .pos = WEB, .gender = 'M'},
        [1] = {.id = 5555, .pos = IOS, .gender = 'F'},
        [2] = {.id = 9999, .pos = SERVER, .gender = 'F'}
    };

    for (int i = 0; i < 3; i++)
    {
        print_developer(&devs[i]);
        unsigned packed = pack_developer(&devs[i]);
        printf("패킹된 값 : %u\n", packed);
        print_packed(packed);
        Developer d;
        unpack_developer(packed, &d);
        printf("\n언패킹 후 결과 확인\n");
        print_developer(&d);
        printf("\n");
    }
    return 0;
}

🖥️출력 결과는 아래와 같아요.

id : 1111
직군 : 0
성별 : M
패킹된 값 : 1111
0 00 00010001010111
GENDER_MASK = 1
POS_MASK = 3
ID_MASK = 3fff

언패킹 후 결과 확인
id : 1111
직군 : 0
성별 : M

id : 5555
직군 : 2
성별 : F
패킹된 값 : 103859
1 10 01010110110011
GENDER_MASK = 1
POS_MASK = 3
ID_MASK = 3fff

언패킹 후 결과 확인
id : 5555
직군 : 2
성별 : F

id : 9999
직군 : 3
성별 : F
패킹된 값 : 124687
1 11 10011100001111
GENDER_MASK = 1
POS_MASK = 3
ID_MASK = 3fff

언패킹 후 결과 확인
id : 9999
직군 : 3
성별 : F


### 비트 필드
앞서 패킹과 언패킹을 하면서, 구조체의 각 멤버를 몇자리의 비트에 저장했어요. 비트 필드를 사용하면, 구조체나 공용체에서 int 또는 unsigned 형의 멤버에 비트 수(폭)를 지정해줄 수 있어요.

> ⚠️주의사항
> - int형 비트 필드는 시스템에 따라 unsigned int 비트 필드로 다루어져요. 따라서, unsigned 비트 필드만을 사용하는 것이 좋아요.
> - 비트 필드 배열은 허용되지 않아요.
> - 비트 필드에 주소연산자 &를 적용할 수 없어요. 이는 비트 필드가 주소를 가지지 않기 때문이에요. 다만 구조체 포인터에서 `->` 연산자로 접근하는 것은 가능해요.
> - 포인터가 직접 비트 필드를 포인트 할 수 없어요.

```c
struct developer
{
	unsigned id: 14;
    unsigned pos: 2;
    unsigned gender: 1;
}

이 때, 비트 수는 멤버 뒤 콜론 다음에 표기해요. 비트 수는 음수가 아닌 정수형 수식으로 지정할 수 있고, 최대값은 각 멤버 변수의 비트 수와 같아요.

컴파일러는 비트 필드가 지정된 구조체의 멤버들을 최소 공간 안에 패킹해요. 비트필드가 지정된 구조체의 크기를 측정할 때는 sizeof 연산자를 사용해야 해요.

struct developer
{
	unsigned id: 14;
    unsigned pos: 2;
    unsigned gender: 1;
}

developer 구조체는 이 경우 4바이트 안에 저장할 수 있어요.

struct developer
{
	char name[30];		// 일반 멤버
	unsigned id: 14;
    unsigned pos: 2;
    unsigned gender: 1;
}

비트 필드를 사용하는 구조체에 일반 멤버 또한 존재할 수 있어요.

컴파일러는 비트 필드를 워드 경계에 걸치지 않게 메모리에 할당해요. 만약 특정 멤버가 워드 경계에 걸치게 되면, 이 멤버의 값을 읽기 위해서 메모리를 2번 읽어와야 하기 때문이에요. 4바이트 워드 시스템에서 아래와 같이 구조체를 선언했어요.

struct abc
{
	int a: 1, b: 16, c: 16;
}

이 경우, abc의 모든 멤버가 한 개의 워드에 저장될 수 없어요. 따라서 컴파일러는 워드 경계에 걸치지 않게 아래와 같이 나눠 저장해요.

  • a, b는 첫 번째 워드에 할당돼요.
  • c는 두 번째 워드에 할당돼요.

패딩과 정렬을 위해, 프로그래머가 임의로 멤버들의 워드 경계를 조절할 수 있어요. 이를 위해서 이름 없는 비트 필드나 폭이 0인 비트 필드를 사용해요.
아래 구조체는 4바이트 워드 컴퓨터에서 i1, i2, i3를 한 개 워드에, i4, i5, i6를 다른 한 개의 워드에 저장해요.

struct small_integers
{
	unsigned i1: 7, i2: 7, i3: 7,
    		: 11,
            i4: 7, i5: 7, i6: 7;
}

아래 구조체는 a, b, c를 모두 다른 워드에 저장해요.

struct abc
{
	unsigned a: 1, :0, b: 1, :0, c: 1;
    // a, b, c는 각각 다른 워드에 할당돼요.
}

이제, 앞선 developer 구조체를 비트필드를 정의해 다시 만들어볼게요.

typedef struct developer
{
	unsigned id: 14;		// 직원 id에요. 4자리의 10진수에요 (0001~9999).
    unsigned pos: 2;	// 업무 분야에요. position 열거형의 값을 가져요.
    unsigned gender: 1;	// 직원 성별이에요. 'M' 또는 'F'의 값을 가져요.
} Developer;

새롭게 작성한 구조체를 패킹하고 언패킹하는 코드를 작성하면 아래와 같아요.

unsigned pack_developer(Developer *data)
{
	unsigned packed = 0;
    packed |= data->id;
    packed |= data->pos << 14;	// id가 14자리 비트이기 때문에, 14자리 비트만큼의 공간을 보존해요.
    packed |= data->gender << 16;	// id와 pos까지의 공간을 보존하려면 총 16자리가 필요하므로, 성별은 16자리 비트 앞에 저장해요.
    return packed;
}

void unpack_developer(unsigned packed, Developer *data)
{
    unsigned GENDER_MASK = 0x1;     // 0b1
    unsigned POS_MASK = 0x3;        // 0b11
    unsigned ID_MASK = 0x3fff;      // 0b11111111111111

    data->id = packed & ID_MASK;
    data->pos = (packed >> 14) & POS_MASK;
    data->gender = (packed >> 16) & GENDER_MASK;
}

gender를 'M', 'F'로 저장할 수 없기 때문에 (gender의 비트 필드 폭이 1이기 때문이에요), 기존 규칙대로 'M'은 0, 'F'는 1로 저장하고 출력 시에 char값으로 출력하게 작성했어요.

void print_developer(Developer *data)
{
    printf("id : %u\n", data->id);
    printf("직군 : %u\n", data->pos);
    printf("성별 : %c\n", data->gender ? 'F' : 'M');
}

// main 함수에서
Developer devs[] = {
	{.id = 1111, .pos = WEB, .gender = 0},
	{.id = 5555, .pos = IOS, .gender = 1},
	{.id = 9999, .pos = SERVER, .gender = 1}
};

이제, 코드를 다시 실행해볼게요.

id : 1111
직군 : 0
성별 : M
패킹된 값 : 1111
0 00 00010001010111

언패킹 후 결과 확인
id : 1111
직군 : 0
성별 : M

id : 5555
직군 : 2
성별 : F
패킹된 값 : 103859
1 10 01010110110011

언패킹 후 결과 확인
id : 5555
직군 : 2
성별 : F

id : 9999
직군 : 3
성별 : F
패킹된 값 : 124687
1 11 10011100001111

언패킹 후 결과 확인
id : 9999
직군 : 3
성별 : F

배운 내용들을 정리해보고 있어요. 잘못 기재된 내용이 있다면, 댓글로 알려주시면 수정할게요.

profile
새내기 대학생 개발자에요 :D

0개의 댓글