이진 숫자는 결국 CPU나 RAM의 트랜지스터에 직접 대응하며, 각 트랜지스터가 "켜짐"인지 "꺼짐"인지를 나타내요.
흔히 "bit-twiddling"이라고 부르는 저수준 조작은 시스템 언어에서 특히 중요해요.
Julia 같은 고수준 언어는 보통 이런 세부 사항 대부분을 추상화해요. 하지만 기본 언어에는 다양한 비트 수준 연산이 마련되어 있어요.
참고: REPL에서 사람이 읽을 수 있는 이진 출력을 보려면, 아래 예제의 거의 모두를 bitstring() 함수로 감싸야 해요.
이렇게 하면 시각적으로 산만해져서, 이 함수를 대부분의 예제에서 생략했어요.
정수 타입은 부호가 있든 없든 1과 0의 문자열로 나타낼 수 있어요.
julia> bitstring(UInt8(5))
"00000101"
비트 시프트는 모든 비트를 지정한 위치 수만큼 왼쪽이나 오른쪽으로 옮기기만 해요.
UInt 타입에서는 한쪽 끝에서 비트 몇 개가 빠져나가고, 다른 쪽 끝은 0으로 채워져요.
julia> ux::UInt8 = 5
5
julia> bitstring(ux)
"00000101"
julia> ux << 2 # left by 2
"00010100"
julia> ux >> 1 # right by 1
"00000010"
왼쪽 시프트를 할 때마다 값이 두 배가 되고, 오른쪽 시프트를 할 때마다 값이 절반이 돼요(버림이 발생할 수 있어요). 이는 십진수로 나타내면 더 분명해요.
julia> 3 << 2
12
julia> 24 >> 3
3
이런 비트 시프트는 "제대로 된" 산술 연산보다 훨씬 빨라서, 저수준 코딩에서 아주 인기 있는 기법이에요.
부호 있는 정수의 경우에는 조금 더 조심해야 해요.
왼쪽 시프트는 비교적 간단해요.
julia> sx = Int8(5)
5
julia> sx # positive integer
"00000101"
julia> sx << 2
"00010100"
julia> -sx # negative integer
"11111011"
julia> -sx << 2
"11101100"
따라서 양수 부호 있는 정수의 왼쪽 시프트는 부호 없는 정수와 같아요.
음수 값은 2의 보수 형태로 저장되는데, 이는 가장 왼쪽 비트가 1이라는 뜻이에요. 왼쪽 시프트에는 문제가 없지만, 오른쪽 시프트를 할 때는 가장 왼쪽 비트를 어떻게 채워야 할까요?
julia> sx >> 2 # simple for positive values!
"00000001"
julia> -sx # negative integer
"11111011"
julia> -sx >> 2 # pad with repeated sign bit
"11111110"
julia> -sx >>> 2 # pad with 0
"00111110"
>> 연산자는 부호 비트를 보존하는 산술 시프트를 수행해요.
>>> 연산자는 숫자가 부호 없는 것처럼 0으로 채우는 논리 시프트를 수행해요.
이전 개념에서 &&(and), ||(or), !(not) 연산자가 불리언 값과 함께 쓰인다는 것을 봤어요.
두 정수의 비트를 비교할 때는 이에 대응하는 연산자인 &(비트 AND), |(비트 OR), ~(물결표, 비트 NOT)를 사용해요.
julia> 0b1011 & 0b0010 # bit is 1 in both numbers
"00000010"
julia> 0b1011 | 0b0010 # bit is 1 in at least one number
"00001011"
julia> ~0b1011 # flip all bits
"11110100"
julia> xor(0b1011, 0b0010) # bit is 1 in exactly one number, not both
"00001001"
여기서 xor()는 함수로 사용되는 배타적 논리합이에요.
친구가 중요한 비밀이 담긴 메시지를 방금 보냈어요. 다른 사람이 쉽게 읽지 못하게 하려고, 그 메시지는 일련의 비트 조작을 거쳐 암호화되었어요. 메시지를 복호화하려면 함수를 작성해야 해요.
메시지를 복호화하는 첫 단계는, 암호화 과정에서 시프트한 비트를 다시 오른쪽으로 시프트해서 되돌리는 거예요. 복호화 과정의 이후 단계들은 왼쪽에서 0이 채워진다고 가정해요.
값과 시프트할 자릿수를 받아 시프트를 수행하는 shift_back() 함수를 구현해요.
shift_back(0b1001, 2) # => 0b0010
다음으로, 1로 설정해야 하는 비트가 있어요.
값과 마스크를 받아, 값의 비트를 1로 설정한 결과를 반환하는 set_bits() 함수를 구현해요.
mask의 비트가 1인 자리에서는 value의 비트도 1로 설정해야 해요.
나머지 비트는 모두 그대로 유지해요.
set_bits(0b0110, 0b0101) # => 0b0111
암호화 과정에서 일부 비트가 뒤집혀요. 메시지를 복호화하려면 이 비트들을 다시 뒤집어야 해요.
값과 마스크를 받는 flip_bits() 함수를 구현해요.
마스크는 값에서 어떤 비트를 뒤집을지 알려줘요.
mask의 비트가 1이면, value의 해당 비트를 뒤집어요.
나머지 비트는 모두 그대로 유지해요.
flip_bits(0b1100, 0b0101) # => 0b1001
마지막으로, 항상 0으로 복호화되는 비트도 있어요.
값과 마스크를 받는 clear_bits() 함수를 구현해요.
마스크의 비트가 1인 자리에서는 value의 비트를 0으로 설정해야 해요.
나머지 비트는 모두 그대로 유지해요.
clear_bits(0b0110, 0b0101) # => 0b0010
Exercism에 가입하고 Julia 트랙을 개념 35개연습 문제 128개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.