Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Programming/C++] 비트 단위 논리 연산의 응용 (with shift bit operator) 본문

Programming/C++

[Programming/C++] 비트 단위 논리 연산의 응용 (with shift bit operator)

scii 2023. 1. 8. 18:43

바탕화면 설정 시에 '16비트(트루컬러)'라는 용어를 접해봤을 것이다. 16비트 컬러의 의미는 점 하나를 표시하는데 16비트를 사용한다는 뜻이다.
즉, 16비트가 표현할 수 있는 상태의 수 만큼이나 다양한 색상을 표현할 수 있다는 의미이다.

컴퓨터 상에서 색상을 표현할 때는 보통 빛의 3요소인 빨강(Red), 초록(Green), 파랑(Blue), 즉 RGB를 기본 원소로 사용한다.
이 R, G, B 각각의 밝기에 따라서 여러 가지 색깔이 만들어지는 것이다. 16비트 컬러의 경우에는 이 R, G, B 각각의 밝기가 16비트 안에 모두 포함되어야 한다.
그래서 R, G, B가 보통 5비트, 6비트, 5비트씩 할당된다 (각각 5비트씩 할당하고 1비트는 쓰지 않는 경우도 있다).

16비트 색상의 구조

그래서 R과 B는 2의5승 단계의 밝기를 표현할 수 있고, G는 2의 6승 단계의 밝기를 표현할 수 있다.


빨간색의 밝기를 30으로 바꿔주려면 어떻게 해야 할까?

#include <bitset>
#include <iostream>

using namespace std;

int main() {
    // 한 점의 색상을 보관하는 변수 (임의의 값을 넣어둔다)
    unsigned short color = 0x1234;

    // 빨간색 부분의 비트들을 0으로 만든다.
    // 0x07ff는 2진수로 0000 0111 1111 1111이다.
    unsigned short color_temp;
    color_temp = color & 0x07ff;

    // 새로운 빨간색의 값을 준비한다.
    unsigned short red = 30;

    // 빨간색의 값을 왼쪽 끝으로 옮긴다.
    unsigned short red_temp;
    red_temp = red << 11;

    // 빨간색의 값을 색상에 넣는다.
    unsigned short color_finished;
    color_finished = color_temp | red_temp;

    cout << "color          = " << bitset<16>(color) << "(" << color << ")" << endl;
    cout << "color_temp     = " << bitset<16>(color_temp) << "(" << color_temp << ")" << endl;
    cout << "red_temp       = " << bitset<16>(red_temp) << "(" << red_temp << ")" << endl;
    cout << "color_finished = " << bitset<16>(color_finished) << "(" << color_finished << ")" << endl;

    return 0;
}


// 결과
color          = 0001001000110100(4660)
color_temp     = 0000001000110100(564)
red_temp       = 1111000000000000(61440)
color_finished = 1111001000110100(62004)

11비트 이동하는 코드가 눈에 띈다. 오른쪽 쉬프트와 마찬가지로 왼쪽으로 쉬프트하는 경우에도 오른쪽에 새로 채워지는 비트 값은 0이다.


부호 있는 값의 쉬프트

사실 쉬프트에는 여러 가지 종류가 있다. 단순하게 비트들을 롬기고 0으로 채우는 쉬프트가 있는 반면에 값의 부호를 유지하면서 비트들을 옮기는 쉬프트가 있다. 
다음의 예제는 똑같은 비트의 나열이 있는 두 변수를 동일하게 쉬프트해도 서로 다른 결과가 나올 수 있다는 사실을 보여준다.

#include <bitset>
#include <iostream>

using namespace std;

int main() {
    unsigned short us = 0xff00;
    short s = (short)0xff00;

    // 동일한 쉬프트 연산 수행
    unsigned short us_shift = us >> 4;
    short s_shift_r = s >> 4;

    cout << "us         = " << bitset<16>(us) << "(" << us << ")" << endl;
    cout << "s          = " << bitset<16>(s) << "(" << s << ")" << endl;
    cout << "us >> 4    = " << bitset<16>(us_shift) << "(" << us_shift << ")" << endl;
    cout << "s >> 4     = " << bitset<16>(s_shift_r) << "(" << s_shift_r << ")" << endl;

    return 0;
}


// 결과
us         = 1111111100000000(65280)
s          = 1111111100000000(-256)
us >> 4    = 0000111111110000(4080)
s >> 4     = 1111111111110000(-16)
unsigned short 타입의 경우에는 새로 추가하는 비트의 값이 0인 반면에 signed short 타입의 경우에는 1의 값으로 설정한다.

 

이러한 차이점을 가져오는 이유는 컴퓨터가 부호를 유지하기 위해서 노력하기 때문이다. 결과에서 괄호 안의 10진수 값을 보면 s의 경우에 쉬프트 연산 후에도 음수를 유지하고 있음을 알 수 있다. 
컴퓨터가 사용하는 음수의 표현 방식 때문에, 왼쪽의 비트들을 1로 채워야만 계속해서 음수로 유지할 수 있게 된다.
그렇다고 해서 부호가 있는 타입의 값을 쉬프트할 때 항상 1로 채워지는 것은 아니다. 위 예제에서 본 것처럼 음수의 값을 오른쪽으로 쉬프트할 때만 1로 채워진다. 왼쪽으로 쉬프트하는 경우나 양수의 값을 오른쪽으로 쉬프트하는 경우에는 부호가 없는 타입과 동일하게 작동한다.


쉬프트 연산

쉬프트 연산의 정의는 비트를 이동하는 것이지만, 결과적으로 곱하기 혹은 나누기를 대신할 수 있다. 왼쪽으로 1비트씩 쉬프트하면 곱하기 2의 효과가 있다. 2비트를 쉬프트하면 곱하기 4의 효과가 있다. 3비트라면 곱하기 8이 된다. 
즉, n비트씩 왼쪽으로 쉬프트하면 곱하기 "2의 n승"한 것과 같은 효과가 생긴다.

3 = 00000011 = 3
3 << 1 = 00000110 = 6 = 3  *2
3 << 2 = 00001100 = 12 = 3 * 4
3 << 3 = 00011000 = 24 = 3 * 8
3 << 4 = 00110000 = 48 = 3 * 16

반대로 오른쪽으로 쉬프트하는 경우에는 2의 n승으로 나눈 효과가 있다. 예를 들어 65280을 오른쪽으로 4비트 쉬프트한 결과로 "2의 4승 = 16"으로 나눈 4080이 된다.

중요한 사실은 곱하기나 나누기를 직접 사용하는 것보다 쉬프트 연산의 속도가 빠르다는 점이다. 그래서 게임 프로그래밍과 같인 연산 속도가 중요한 곳에서는 쉬프트 연산을 대신해서 사용한다. 

예를 들어 어떤 변수에 320을 곱해야 하는 경우에 다음과 같이 사용하기도 한다.

(x << 8) + (x << 6)

위의 식을 풀게 되면 다음과 같다.

(x * 2의 8승) + (x * 2의 6승)
(x * 256) + (x * 64)
x * (256 + 64)
x * 320

결국, x * 320이 되지만 쉬프트 연산을 사용한 경우에 더 빨리 수행된다.

Comments