F.R.I.D.A.Y.

비트 연산자 : 메모리 크기 줄이기 본문

DEV/C C++

비트 연산자 : 메모리 크기 줄이기

F.R.I.D.A.Y. 2019. 12. 21. 23:47
반응형

https://pixabay.com/illustrations/binary-code-binary-binary-system-475664/

 최근 비트 연산자에 대한 질문을 들어온지라, 오늘은 비트 연산자에 대해 알아봅니다.

 

더보기

# 들어가기에 앞서..

 비트 연산자는 프로그래밍에 있어 고급 기술이라 분류할 수 있을 것 같습니다. 따라서 이해도 안 되는데 처음부터 배울 필요는 없습니다. 그래도 배워두면 프로그래밍 능력의 초석을 단단히 다질 수 있을 것이란 말을 드리고 싶습니다.


비트 알아보기

 비트 연산자를 알아보기 전에, 우리는 비트에 대해 알아볼 필요가 있습니다. 비트란 정보의 가장 최소의 단위로서 이 비트가 8개 모여 1바이트가 됩니다. 예를 들어 아래 값이 char 타입의 변수에 들어있다고 합시다.

 

' 15 ' 

 이 숫자 15의 비트 패턴은 어떻게 될까요?

 

#include <stdio.h>

int main(void){
	
	unsigned char ch = 15;
	
	printf("%d\n", ch);

	for(int i = 0 ; i< 8;++i){
		printf("%d번 째 비트 패턴: %d\n", i, (unsigned char)(ch & 1 << i) >> i);
	}
	
	return 0;
}

 이 코드를 사용하면 15라는 값에 대해 각 자리의 비트 패턴을 확인할 수 있습니다. 15의 비트 패턴은 다음과 같습니다.

' 0000 1111 '

 

더보기

# 비트를 읽을 때 주의하세요.

 char 타입 기준으로 가장 앞에 있는 비트를 7번 비트, 마지막에 있는 비트를 0번 비트라고 부릅니다.

 따라서 15의 비트 패턴은 다음 이미지와 같습니다.

이미지상 각 공간에 위치한 [7, 6, 5, 4, 3, 2, 1, 0]은 메모리 1바이트를 8개. 즉, 8비트로 나누었을 때 해당 비트의 순서입니다. 각 비트는 2^n의 값을 가집니다. 


연산자의 종류

 비트 연산자의 종류는 아래 포스트를 참고하세요.

 

비트 연산자 : 종류

비트 연산자의 종류와 연산 방법을 알아봅니다. & (비트 AND, 비트곱) 비트 AND연산자는 양쪽 피연산자(operand) 모두 참값이어야 1을 반환했던 논리 AND(&&) 연산자와 비슷합니다. 양쪽 두 값의 동일 위치에 존..

pang2h.tistory.com


어디에 사용할까

 비트 연산자는 다수의 운영체제가 메모리를 관리하는 단위(1바이트) 보다 더 작은 데이터를 다루는 연산자임과 동시에, 생소한 이진 값을 다룬다는 점에서 다른 연산자보다 더 어려운 것이 사실입니다. 그러나 이 비트 연산자는 그 쓰임에 있어 여느 연산자처럼 중요하게 다뤄지고 있습니다.

 

 비트 연산자는 현실에선 다양한 영역에서 사용되고 있습니다. 대표적으로 압축, 영상, 음성, 암호화 영역 등에서 사용하고 있습니다. 그 외에도 메모리를 더 효율적으로 관리해야 하는 프로그램에서도 사용하고 있습니다.

 

 이번 포스트에서는 메모리를 더 효율적으로 다루는 코드를 이용해 비트 연산자를 다뤄보겠습니다. 앞서 말했듯이 연산자 자체가 어렵기 때문에 코드를 보고 "이 코드는 무슨 기능을 한다"라고 바로 알아보기 힘들 수 있습니다. 남이 짠 코드가 해석하기 힘든 것도 있지만 비트 연산자라서 더 심할 수 있습니다.


Base Code.

 가장 극단적인 예를 들기 위해 이 코드에서는 Boolean(True, False) 타입에 넣을 수 있는 데이터를 이용하겠습니다.

 

 우리는 어떤 기계의 스위치를 관리해야 합니다. 그래서 아래와 같이 스위치의 상태를 기록할 수 있는 프로그램을 만들었습니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num;
    int *arr;

    printf("몇 개의 스위치 관리하겠습니까? ");
    scanf("%d", &num);

    arr = (int *)malloc(sizeof(int) * num); // C6011 경고 발생 가능 : https://pang2h.tistory.com/196

    for(int i = 0 ; i <num; ++i){
        printf("%d 번째 스위치 상태를 입력하세요. ON 상태는 1, OFF 상태는 0입니다.\n"
               "%d번 스위치 상태: ", i + 1, i + 1);
        scanf("%d",arr + i);
    }
    
    free(arr);
    
    return 0;
}

 이 코드는 굉장히 비효율적으로 메모리를 사용하고 있습니다. 400개의 스위치를 관리한다고 가정합니다. 그렇다면 400개의 스위치 상태를 저장하는데 드는 메모리는 몇 바이트일까요?

 

4(type, int) * 400(count) = 1600Byte == 1.56KB

 

 고작 스위치 관리하는데 1.5KB나 사용하게 됩니다. T/F로 판별할 수 있는 값을 저장하기 위해서. 작은 값이라 생각하실지라도 거대한 타워 같은 곳에서 고작 400개의 스위치만을 관리하지는 않을 것입니다. 각종 전등부터 시작해서 스피드게이트, 엘리베이터, 에스컬레이터까지. 스위치로 조작할 구조물들은 굉장히 많습니다. 게다가 혹시 모를 상황에 대비해 백업 데이터도 남겨야 하니 그 크기는 더 커질 것입니다.

 

 우리는 이제 이 비효율적인 코드를 뜯어고쳐 최대 1/32 크기로 낮출 겁니다. 어떻게 낮추느냐고요? 비트 연산자를 활용하면 가능합니다.

 

# 베이스 코드에서 변수 타입을 int로 작성한 것은 대체적으로 값을 저장할 때 int를 사용하기 때문입니다.


코드 뜯어고치기

 Base Code에서 몇 가지 부분을 수정해봅니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num;
    int arrSize;       // 변경!
    unsigned int *arr; // 변경!

    printf("몇 개의 스위치 관리하겠습니까? ");
    scanf("%u", &num);
    
    arrSize = num / 32 + (num % 32 != 0); // 변경!
    
    arr = (unsigned int *)malloc(sizeof(unsigned int) * (arrSize));

    for(int i = 0 ; i <num; ++i){
        printf("%d 번째 스위치 상태를 입력하세요. ON 상태는 1, OFF 상태는 0입니다.\n"
        		"%d번 스위치 상태: ", i + 1, i + 1);

    }
    
    return 0;
}

 자료형 int는 32개의 비트가 모여 만들어진 크기와 동일한 크기를 가지고 있으므로 할당받는 공간은 32로 나누어 올림 해줍니다. 그렇게 되면 할당받은 메모리는 관리할 스위치보다 최대 31bit 클 뿐입니다.

더보기

# 왜 31bit까지만 낭비되나요?

 예시로 33개의 스위치를 관리한다고 생각해봅니다.

 그렇다면 33번 스위치를 가지고 있는 4바이트 메모리는 하나의 비트만을 사용하게 됩니다. 따라서 31개의 비트가 남게 되죠.

 

 이제 각 비트에 접근할 수 있도록 코드를 구성해줍니다.

void ON(int* target, int nBit){
	int index = nBit / 32;
	unsigned int bitMask = 1 << (nBit % 32);
	
	target[index] |= bitMask;
}

void OFF(int* target, int nBit){
	int index = nBit / 32;
	unsigned int bitMask = 1 << (nBit % 32);
	
	target[index] &= ~(bitMask);
}

 일반적으로 이러한 작업은 단순 연산에 불과한 작업이기 때문에 함수 호출에 의한 오버헤드를 줄이기 위해 매크로 함수로 작성하는 경우가 많습니다. 인라인 함수들도 있으니 참고하세요.

더보기

# 매크로 함수로 작성하기

 ON, OFF 함수는 이렇게 작성할 수도 있습니다.

#define  ON(x, nBit) (x[nBit / 32] |= 1 << (nBit % 32))
#define OFF(x, nBit) (x[nBit / 32] &= ~(1 << nBit % 32))

 

 이렇게 작성한 코드는 아래와 같습니다.

#include <stdio.h>
#include <stdlib.h>

void ON(int* target, int nBit){
	int index = nBit / 32;
	unsigned bitMask = 1 << (nBit % 32);
	
	target[index] |= bitMask;
}

void OFF(int* target, int nBit){
	int index = nBit / 32;
	unsigned bitMask = 1 << (nBit% 32);
	
	target[index] &= ~(bitMask);
}

int main(void){
    int num;
    unsigned int *arr;
    int arrSize;
	
    printf("몇 개의 스위치 관리하겠습니까? ");
    scanf("%d", &num);
    arrSize = num / 32 + (num % 32 != 0);
    arr = (unsigned int *)malloc(sizeof(unsigned int) * num);

    for(int i = 0 ; i <num; ++i){
        printf("%d 번째 스위치 상태를 입력하세요. ON 상태는 1, OFF 상태는 0입니다.\n"
               "%d번 스위치 상태: ", i + 1, i +1);
        int temp = 0;
        scanf("%d", &temp);
		
        if(temp){
            ON(arr, i);
        }else{
            OFF(arr, i);
        }
    }
	
    return 0;
}

 

 이전 코드에서는 스위치 하나의 상태를 저장하기 위해 4바이트의 공간을 사용했지만, 다시 작성한 코드에서는 4바이트의 공간에 무려 32개의 스위치 상태를 저장할 수 있게 되었습니다.

 

 처음 이론적으로 400개의 스위치 상태를 관리해야 한다고 했을 때, 약 1.5KB의 공간이 필요했습니다. 이제 새로 만들어진 코드로 다시 계산해보겠습니다.

 

4(type, int) * 400(count) / 32(count of bits in int) = 50Byte == 0.04KB

 

 이론상으론 32배가 맞습니다. int는 4바이트이므로 13개의 int 공간(52Byte)이 필요합니다. 이를 바탕으로 계산하면 실제 값은 1600 / 52 = 30.76으로 이론상 축소 값과 큰 차이가 없습니다.


추가로

 극단적인 예시를 들기는 하였지만 비트 연산자를 잘 알아서 사용하고 안 하고는 프로그래밍에 있어 천지차이입니다. 당장에 Windows 프로그래밍을 하더라도 컨트롤에 대한 설정값들이 비트 연산자를 사용하도록 되어 있습니다.

 

Windows API - 나무위키

이 저작물은 CC BY-NC-SA 2.0 KR에 따라 이용할 수 있습니다. (단, 라이선스가 명시된 일부 문서 및 삽화 제외) 기여하신 문서의 저작권은 각 기여자에게 있으며, 각 기여자는 기여하신 부분의 저작권을 갖습니다. 나무위키는 백과사전이 아니며 검증되지 않았거나, 편향적이거나, 잘못된 서술이 있을 수 있습니다. 나무위키는 위키위키입니다. 여러분이 직접 문서를 고칠 수 있으며, 다른 사람의 의견을 원할 경우 직접 토론을 발제할 수 있습니다.

namu.wiki

 가장 손쉽게 기본적인 Windows 프로그래밍 소스코드를 보면, WndProc 함수에서 아래 밑줄 친 부분은 비트 연산자를 이용합니다.

 이렇게 비트 연산자를 사용하는 이유는 이미지와 같이 설정 값들이 비트 AND(&) 연산을 했을 때 0이 나오는. 달리 말해 하나의 비트만 1인 값으로 설정되어있기 때문입니다.

https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles

 Windows API들이 비트 연산자를 이용해 정보를 넘기도록 하는 이유는 함수를 잘 만들기 위함입니다. 만일 FuncA라는 함수에 속성 데이터가 10개 있는데 이 10개의 정보를 각각의 인자로 넘기게 되면 이 함수는 10개의 인자가 들어가야 합니다. 쓰지도 않을 속성인데 속성이 10개가 있다는 이유만으로 NULL을 수없이 붙여야 하는 것입니다.

속성 예시
A B C D E
F G H I J

 [A-J]의 총 10개의 속성이 존재한다고 했을 때 우리는 <A>, <C> <F> 총 세 가지의 속성만 설정하기를 원합니다. 그렇다면 비트 연산자와 일반 인자로 전달하는 방식은 아래와 같습니다.

// 인자로 전달하는 방식의 함수 사용법
FuncA(A, NULL, C, NULL, NULL, F, NULL, NULL, NULL, NULL);

// 비트 연산자를 활용한 함수 사용법
FuncA(A | C | F);

 

 비트 연산자 사용 유무에 대한 차이를 아시겠나요?


더 읽어보기

 방금 설명한 함수 인자에 비트 연산자를 활용하는 이유입니다.

 

비트 연산자 : 함수에 인자 넘기기

비트 연산자 : 메모리 크기 줄이기 최근 비트 연산자에 대한 질문을 들어온지라, 오늘은 비트 연산자에 대해 알아봅니다. 더보기 # 들어가기에 앞서.. 비트 연산자는 프로그래밍에 있어 고급 기술이라 분류할 수..

pang2h.tistory.com

# index

728x90
반응형

'DEV > C C++' 카테고리의 다른 글

구조체(struct) part1. default  (0) 2020.01.02
비트 연산자 : 함수에 인자 넘기기  (0) 2019.12.31
비트 연산자 : 종류  (0) 2019.12.20
void 포인터(메모리)  (0) 2019.12.08
calloc의 인자는 왜 두 개가 필요할까?  (1) 2019.12.05
Comments