C, C++ 문법

비트 연산의 이해와 활용

디버그정 2011. 7. 10. 21:48


비트연산자에는 다음과 같은 것들이 있다.
~
&
|
^
<<
>>

~, &, | 연산자의 경우는 사용도가 높고 직관적으로 이해가 잘 된다.
훌륭한 사용예로 윈도우 스타일 변경을 들 수 있다. GetWindowLong, SetWindowLong으로 얻거나 변경이 가능하다.
위 API에서 스타일 결과값이나 인수는 DWORD 형 4바이트로 특정 비트에 각각의 속성을 매칭시키는 식이다.
윈도우 스타일뿐 아니라 소위 각종 속성이나 플래그는 이런 식으로 처리한다.
가령 WS_VISIBLE 속성은 총 32비트 중에 어느 곳을 지정해서 그곳의 비트가 1이면
해당속성을 가진 것으로 인식하는 것이다. 스타일이 4바이트(32비트)면 총 32개 속성을 주는 게 가능하다.
여담으로 이게 부족했던지 CreateWindowEx에서는 첫번째 인수에 스타일 인수를 하나 더 줘서
총 64개까지 속성 설정이 가능하게 되었다.

스타일을 주고자 할때
dwStyle | WS_VISIBLE
빼고자 할 때는
dwStyle & ~WS_VISIBLE
이런 식으로 연산을 해주면 된다.

참고로 빼고자 할때 xor 연산을 활용하여
(dwStyle | WS_VISIBLE) ^ WS_VISIBLE 도 가능하다.
이 때 괄호는 반드시 써야된다. 안쓰면 뒤의 ^부터 연산해버린다. 연산자 우선순위에 주의해야 된다.

위에서 |연산을 하지 않고 단순 ^만 쓰면 토글기능을 하게 된다.
(괄호 안의 |연산을 먼저 한 이유는 저 속성이 이미 기존에 없는 경우도 대비해서이다.)
dwStyle ^ WS_VISIBLE  이렇게 하면 해당 비트가 항상 반전이 일어나므로 스타일에서 해당속성이 무조건 반대로 변할 것이다.
아래에서 보겠지만 ^연산을 1과 하는 부분은 항상 비트반전이 일어난다.

^(xor)연산의 경우는 그렇게 자주 사용하지 않아서인지 코딩할 기회가 많지 않고 직관적으로 이해가 쉽지 않다.
그런데 그래픽 관련 처리를 하고자 할 때나 해당 연산이 사용되는 소스를 보는 경우 숙지할 필요가 있다.
^ 연산은 비교되는 두 비트가 같은 경우 0, 다른 경우 1이다.
각각의 경우는 다음과 같은 결과를 낳는다. 1과 1연산 경우 제외하고 |(or) 연산과 결과가 같다.
1) 0^0 ->0
2) 1^0->1,   0^1->1
3) 1^1->0
풀네임으로 eXclusive OR이고 수학적으로 배타적 논리합이라고 한다. 둘 중 하나만 참인 경우 참이라는 의미이다.
(합이기 때문에 0 + 0은 합이 없으니 0이고, 배타적 성질로  1, 1처럼 서로 참이라 주장하는 경우 배타성이 적용되어서 거짓(0)이 된다고 이해하면 쉽다.)
이로부터 자주 활용되는 원리로
0과 ^(xor)연산하는 경우 그 대상의 비트는 반전 없이 그대로 유지된다.
(실상 이 경우는 |(or)연산하는 것과 같은 결과가 된다. A^0 과 A|0은 같다. 항상 자기자신이다.)
이를테면 위의 1)과 2)의 경우 0과 연산하는 대상의 값이 그대로 유지되고 있음을 볼 수 있다.
이런 원리는 아이콘이나 움직이는 애니메이션에서 배경색을 유지하는데 사용한다.
아이콘의 경우 배경을 투명하게 처리할려면 비트맵 2개가 필요하다.
마스크 비트맵과 컬러 비트맵이다.

  

좌측이 마스크비트맵,      우측이 컬러비트맵이다.

참고로 검은색은 0, 하얀색은 모조리 비트가 1이다.
가령 3바이트 색체계에서 검은색은 0 하얀색은 0xFFFFFF값이다.

일단 배경그림과 마스크비트맵을 &연산하면
마스크비트맵에서 검은색 부분: 검은색(모든 비트 0)으로 &연산해봐야 모조리 0이 되어 검은색이다.
마스크비트맵에서 하얀색 부분: 하얀색(모든 비트 1)으로 &연산하면 배경그림의 비트를 그대로 유지하게 된다.
&연산 결과 배경그림에서 마스크비트맵의 검은색 부분만 검게 칠해진 채로 있게 된다.

그 다음에 컬러비트맵과 위에서 처리된 배경그림을 xor연산하면 어떻게 될까?
앞에서 말했듯이 검은색 즉 0과 xor 연산하면,,, 그 대상의 비트는 고스란히 유지된다.
이 결과 컬러비트맵의 바깥 검은색 부분은 xor 연산시 배경그림의 부분이 그대로 유지된다. 
또한 컬러비트맵의 방패 부분은 앞에서 배경그림에서 검은색 칠해진 부분과 xor연산이 되므로
연산결과 방패모양이 그대로 그려지게 될 것이다.
참고로 컬러비트맵을 or연산을 해도 마찬가지 결과이다.(A^0, A|0, A는 모두 같다.)
단지 윈도우 시스템에서 아이콘의 경우 마스크비트맵은 and연산, 컬러비트맵은 xor연산으로 처리하고 있다.

위 원리를 그래픽 처리 관점에서 표현을 바꾸면
"검은색과 대상 색을 ^(xor)연산하면 그 대상 색은 그대로 유지된다."이다.

참고로 하얀색과 대상 색을 &연산해도 대상 색은 그대로 유지된다.
그리고 검은색과 대상 색을 |연산해도 대상 색은 그대로 유지된다.
위 둘은 직관적으로 파악 가능하다. |연산과 &연산을 해서도 위와 같은 효과를 낼 수 있다.
이 경우 마스크비트맵에서 하얀색과 검은색을 반대로 하고, 컬러비트맵에서는 바깥 검은색을 하얀색으로 바꾸면 된다.
적용과정에서는 배경과 마스크비트맵을 |연산하고 그 후 컬러비트맵을 &연산하면 된다.

다음으로 #define 스왑 매크로나 암호화 및 복호화 알고리즘,
이미지 반전 후 원래 상태 회복(ex)SetRop2에서 R2_XORPEN 플래그 사용) 등에 사용되는 원리가 있다. 
어떤 소스를 대상과 xor연산하고 그 결과를 다시 대상과  xor연산하면
원래의 소스로 돌아온다.

이를테면 (A^B)^B는 A이다.
일견 장황해 보이고 파악이 쉽지 않아 보이지만 경우를 따져보면 그리 어렵지 않다.
비트별로 따지면 된다. 0, 1 두개의 경우밖에 없으므로 2개를 연산할 경우 총 4가지 경우밖에 없다.
A가 0, B가 0이면    0^0 -> 0   이 결과를 다시 B와 연산   0^0 -> 0이다. 원본 A의 0 유지
A가 0, B가 1이면    0^1 -> 1   이 결과를 다시 B와 연산   1^1 -> 0이다. 원본 A의 0 유지
A가 1, B가 0이면    1^0 -> 1   이 결과를 다시 B와 연산   1^0 -> 1이다. 원본 A의 1 유지
A가 1, B가 1이면    1^1 -> 0   이 결과를 다시 B와 연산   0^1 -> 1이다. 원본 A의 1 유지

이것을 비트부호가 바뀌는 반전 측면에서 풀어쓰자면(^의 좌항(좌측) 기준에서)
0, 0인 경우 xor연산은 항상 0이다. 반전 없음
1, 0인 경우 1^0은 반전 없이 1이고  다시 1^0 해봐야 반전 없이 1이다.
0, 1인 경우 0^1은 반전이 일어나 1이 되고 1^1 또한 반전이 일어나 다시 0이 된다.
1, 1인 경우 1^1은 반전이 일어나 0이 되고 0^1 또한 반전이 일어나 다시 1이 된다.

위에서 보듯이 좌항 기준으로 ^의 우항(우측)이 0이면 반전이 없고 1이면 반전이 일어난다.
우항이 0이면 항상 반전이 없으므로 값이 유지된다.
우항이 1이면 항상 반전이 일어나고 저 경우 2번 반전이 되어 원래값이 된다.
반전의 반전은 원래 값이므로 짝수번(2번, 4번, 6번, 8번...) 반복하면 항상 원래값으로 돌아오게 된다.
우항 기준으로 삼아도 마찬가지로 좌항이 0이면 반전이 없고 1이면 반전이 일어난다.
^(xor)연산을 0과 하면 항상 반전이 없고 ^(xor)연산을 1과 하면 항상 반전이 일어난다.
xor 연산에서는 1과 xor되는 부분만 반전이 일어나므로 전체를 반전시키는 ~연산과 달리 선택적 반전이 일어난다.

참고로 스왑매크로가 한 줄로 간결하고 좋아 보이지만 그렇지 않다.
#define SWAP(a, b) ((a)^=(b)^=(a)^=(b)) 또는 #define swap(x, y) {x=x^y; y=x^y; x=x^y;}
위처럼 표현식이 간결한만큼 속도가 더 빠르다고 착각할 수 있는데,
비트연산이 수 번 일어나므로 단순한 대입 3번보다 느리고, 정수형태만 가능하다는 단점도 있다.
대입식 스왑은 비트연산이 필요없고 실수나 구조체 등 다양한 형에 사용가능하다.

참고로 백과사전 홈페이지에서 찾아본 xor 법칙은 다음과 같다.
XOR 이항 연산은 다음과 같은 성질을 갖는다.
L1. 교환법칙: A^B와 B^A는 같다.
L2. 결합법칙: (A^B)^C와 A(^B^C)는 같다.
L3. 항등원의 존재: 어떤 A에 대하여서도, A^Z는 A가 되는 Z가 존재한다. Z는 0이다. 
L4. 역원의 존재: A^X가 항등원 Z(위에서 0)가 되는 X가 존재한다. 역원은 사실 자기 자신 A이다.
3번 법칙이 먼저 활용예로 제시한 아이콘이나 그림 배경 투명화처리 등에 사용되고,
또한 A^B^B 값이 A라는 것을 위 법칙들을 사용해서 논리적으로 증명할 수도  있다.
(A^B)^B는 2번 결합법칙에 의해 A^(B^B)와 같고 여기서 우항 (B^B)는 법칙4에 의해 0이 되고
결국 A^0 연산이 되고 이는 법칙3에 의해 A와 같게 된다.
4번 원리의 경우 어셈블리 코드에서 레지스터에 0 입력할 때 자주 보는 코드이다.
xor eax,eax면 eax 레지스터의 값이 0이 된다.

<< >> 시프트 연산의 경우 지정된 수만큼 비트를 이동시킨다.
<<은 왼쪽, >>는 오른쪽 이동으로 화살표와 이동방향이 같으므로 직관적으로 파악가능하다.
정수와 문자열 간의 상호변환 등에서 사용할 수 있고 MAKELONG, MAKEPARAM, HIWORD 등의 매크로 등에 쓰인다. 
참고로 곱셈식에서 정수형인 경우 <<1이 X2와 같고, <<2가 X4와 같으므로,,,즉 2의 n제곱 식이 <<n과 같은 결과이다.
속도 최우선 코딩이라면 산술식을 저런 식으로 바꿀 수도 있을 것이다.
그런데 전에 시프트 연산을 곱셈, 나눗셈 대체해 산술식에 쓴 경우가 있었는데(<<1은 곱하기2, >>1을 나누기2 대체로...)
컴파일 과정에서 저런 부분을 다르게 최적화시켜서 처리해서인지 제대로  결과가 안 나온 적이 있다.
저렇게 쓰는 경우 제대로 작동하는지 꼭 테스트해 볼 필요가 있는 듯하다.