이번 편은 비트 수준으로 데이터를 다뤄보는 이야기에요. 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"라는 구조체를 패킹하고자 해요. 각 멤버별로 몇자리 비트에 저장해야하는지 계산해볼게요.
멤버 | 비트 자리 수 | 설명 |
---|---|---|
.id | 14 | 최대값인 9999가 00100111 00001111 이니, 14자리 비트가 필요해요. |
.pos | 2 | position 열거형은 0~3의 값을 가지니, 2자리 비트가 필요해요. |
.gender | 1 | char형 값이지만, 실제로는 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
함수에서 구조체를 패킹할 때 비트열을 다음과 같이 배치했어요.
1 | 2 | 14 |
---|---|---|
.gender | .pos | .id |
이 순서대로 패킹된 비트열을 잘라서 이해해볼게요.
0 00 00010001010111
패킹된 정보는 사용하기 전에 다시 언패킹 해야 해요. 이전에 배웠던 마스킹 연산을 사용해, 패킹된 비트열에서 구조체의 각 멤버에 해당하는 부분의 비트열만 잘라 읽어야 해요.
앞선 예제에서, developer 구조체는 아래와 같이 패킹되었어요.
1 | 2 | 14 |
---|---|---|
.gender | .pos | .id |
그렇다면, 이 경우 필요한 마스크는 아래와 같아요.
멤버 | 마스크 |
---|---|
.gender | 1개의 1 |
.pos | 2개의 1 |
.id | 14개의 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의 모든 멤버가 한 개의 워드에 저장될 수 없어요. 따라서 컴파일러는 워드 경계에 걸치지 않게 아래와 같이 나눠 저장해요.
패딩과 정렬을 위해, 프로그래머가 임의로 멤버들의 워드 경계를 조절할 수 있어요. 이를 위해서 이름 없는 비트 필드나 폭이 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
배운 내용들을 정리해보고 있어요. 잘못 기재된 내용이 있다면, 댓글로 알려주시면 수정할게요.