Out of Bedlam Swiftish

스위프트 비트 다루기

스위프트의 비트단위 연산이 궁금하던 차에 관련 포스트를 발견하여 정리해 보았습니다.

참고: https://www.uraimo.com/2016/02/05/Dealing-With-Bit-Sets-In-Swift/

스위프트의 고정 크기 정수형과 비트 연산자들을 사용하여 비트를 다루는 것은 상당히 직관적으로 보입니다.

하지만 스위프트 언어 자체와 표준 라이브러리가 항상 안전성을 우선으로 하여 비트와 다른 정수형을 같이 다루어야 하기 때문에 다른 언어를 사용할 때 보다 추가적인 형 변환이 필요합니다.

정수형과 비트단위 연산자

스위프트에는 크기와 부호에 따라 여러가지 정수형을 지원합니다: Int/UInt, Int8/UInt8(8비트), Int16/UInt16(16비트), Int32/UInt32(32비트), Int64/UInt64(64비트).

IntUInt는 플랫폼에 따라 달라지는데, 32비트 플랫폼에서는 Int32/UInt32와 동일하며, 64비트 플랫폼에서는 Int64/UInt64와 동일하게 됩니다. 그 외의 정수형들은 플랫폼에 관계없이 지정된 크기를 가지게 됩니다.

고정 크기의 자료형들은 비트단위 연산자들과 함께 사용될 때 그 자료형의 데이터 크기가 명확하게 만들기 때문에 편리합니다. 그러므로 비트단위 연산을 할 때 플랫폼에 따라 크기가 달라지는 Int 또는 UInt는 거의 사용하지 않게 될 것입니다.

고정크기 정수형의 변수들은 2진수, 8진수, 16진수 값으로 초기화 할 수 있습니다.

var int1:UInt8 = 0b10101010
var int2:UInt8 = 0o55
var int3:UInt8 = 0xA7

비트 단위 연산자에는 단항 연산자인 NOT (~)연산자를 포함하여 AND(&), OR(|), XOR(^), 좌/우 시프트(<<, >>)가 있습니다.

부호가 없는 정수는 좌/우 시프트의 경우에 주어진 수만큼 반대편 방향으로부터 0을 끼워넣습니다. 하지만 부호가 있는 정수의 경우에는 부호 비트를 유지한다는 점에 유의해야합니다.

한 바이트 이상의 정수형들에는 엔디언 변환을 위한 연산 프로퍼티(computed properties)을 제공합니다. littleEndian, bigEndian, byteSwapped는 현재의 정수 표현법으로 부터 리틀엔디언, 빅엔디언으로 변경하고 엔디언 간의 변환을 합니다.

아래와 같은 방법으로 32 또는 64 비트 플랫폼을 구별할 수 있습니다.

strideof(Int) == strideof(Int32)

여기서 strideof를 사용하였지만 sizeof를 사용해도 동일합니다.

참고: 스위프트의 sizeof의 의미는 C/Objective-C와 다릅니다. 스위프트에서 sizeof는 하나의 인스턴스를 저장하는데 필요한 공간을 바이트 수로 나타내지만 C와 Objective-C의 sizeof는 스위프트의 strideof와 같으며 배열상에서 연속된 인스턴스 간의 거리를 뜻합니다.

정수형 변환

서로 다른 자료형들을 섞어서 산술 연산을 하도록 시도해 보았다면 스위프트가 암시적인 형 변환을 하지않는다는 것을 알게 되었을 것입니다. 명시적으로 표현식의 변수들을 변환해야 합니다.

표현식안에서 다수의 정수들을 사용하는 경우에, 스위프트는 정수 리터럴의 자료형은 다른 형이 지정된 변수와 함께 사용된 경우에만 유추할 수 있습니다.

var u8:UInt8 = 1
u8 << 2           //4: 숫자 2는 UInt8로 간주되어 왼쪽으로 2자리 시프트합니다.

var by2:Int16 = 1
u8 << by2         //에러: 피연산자들이 서로 다른 형입니다.
u8 << UInt8(by2)  //4: 수동으로 형변환 하였습니다. 하지만 안전한 방법이 아닙니다.

왜 안전한 방법이 아니라고 하는 걸까요?

스위프트에서는 더 큰 크기의 정수형을 작은 정수형으로 변환하거나 부호 없는 정수를 부호가 있는 정수로 변환할때, 값의 오버플로우가 발생하게 되면 넘치는 비트를 잘라내는 동작을 하지 않고 그대로 런타임 오류를 발생시키기 때문입니다.

사용자가 입력한 값이거나 외부 콤퍼넌트로 부터 전달받은 값을 다룰 때 이 점을 명심하여 주의해야 합니다.

다행히도 스위프트에는 비트 연산을 할 때 유용한 생성자 init(truncatingBitPattern:)을 사용하는 비트 절삭 변환 방법이 있습니다.

var u8:UInt8=UInt8(truncatingBitPattern:1000)
u8 // 232

이 예제에서는 이진법으로 0b0b1111101000으로(10자리, 2바이트) 표현되는 정수 1000을 UInt8로 변환하면서 하위 8비트만 유지하고 그 외의 부분은 모두 버리도록 하였습니다. 그결과 232가 남게 되는데 이진법으로는 0b11101000(8자리, 1바이트)입니다.

Intn과 UIntn형의 모든 조합에 동일하게 적용할 수 있습니다. 부호가 있는 Int형의 부호는 무시되며 단순히 비트열의 일부로 간주되어 처리됩니다. 같은 크기의 부호있는 정수와 부호 없는 정수사이의 변환에는 init(bitPattern:)을 사용할 수 있으며 보통의 잘삭 변환과 동일한 결과를 가져옵니다.

유추를 허용하지 않는 안전성을 우선하는 접근 방식의 유일한 단점은 많은 형 변환 과정을 직접 처리해야한다는 점입니다. 형변환 코드를 일일이 작성해야합니다.

하지만 스위프트에서는 기본 자료형에 대해서 새로운 메서드를 추가할 수 있고 특정 크기의 정수을 다른 크기의 정수형으로 변환하는 유틸리티 메서드를 만들 수 있습니다.

extension Int {
    public var toU8: UInt8{ get{return UInt8(truncatingBitPattern:self)} }
    public var to8: Int8{ get{return Int8(truncatingBitPattern:self)} }
    public var toU16: UInt16{get{return UInt16(truncatingBitPattern:self)}}
    public var to16: Int16{get{return Int16(truncatingBitPattern:self)}}
    public var toU32: UInt32{get{return UInt32(truncatingBitPattern:self)}}
    public var to32: Int32{get{return Int32(truncatingBitPattern:self)}}
    public var toU64: UInt64{get{
            return UInt64(self) //No difference if the platform is 32 or 64
        }}
    public var to64: Int64{get{
            return Int64(self) //No difference if the platform is 32 or 64
        }}
}

extension Int32 {
    public var toU8: UInt8{ get{return UInt8(truncatingBitPattern:self)} }
    public var to8: Int8{ get{return Int8(truncatingBitPattern:self)} }
    public var toU16: UInt16{get{return UInt16(truncatingBitPattern:self)}}
    public var to16: Int16{get{return Int16(truncatingBitPattern:self)}}
    public var toU32: UInt32{get{return UInt32(self)}}
    public var to32: Int32{get{return self}}
    public var toU64: UInt64{get{
        return UInt64(self) //No difference if the platform is 32 or 64
        }}
    public var to64: Int64{get{
        return Int64(self) //No difference if the platform is 32 or 64
        }}
}

var h1 = 0xFFFF04
h1
h1.toU8   // Instead of UInt8(truncatingBitPattern:h1)

var h2:Int32 = 0x6F00FF05
h2.toU16  // Instead of UInt16(truncatingBitPattern:h2) 

평범한 비트 패턴

다중-바이트 구성요소 추출

연속된 바이트열에서 AND와 오른쪽 시프트 연산자를 조합하여 특정 비트나 바이트를 추출하는 것은 널리 사용되는 방법입니다. 다음의 예는 RGB 컬러에서 컬러 구성을 추출하는 예입니다.

let swiftOrange = 0xED903B
let red = (swiftOrange & 0xFF0000) >> 16    //0xED
let green = (swiftOrange & 0x00FF00) >> 8   //0x90
let blue = swiftOrange & 0x0000FF           //0x3B

비트마스크와 AND 연산으로 비트마스크에서 지정된 범위에 있는 비트가 1인 경우에만 1을 남기고 그 외의 경우는 모두 0으로 만들어서 원하는 비트들만 고립시킵니다. 그리고 오른쪽으로 16자리 시프트하여 레드 콤포넌트의 값을 얻게됩니다. 그린 콤포넌트는 8비트 이동시켜 얻게 됩니다. 이렇게 AND+시프트 패턴을 어플리케이션 전역에서 사용할 수도 있습니다만, 코드의 가독성이 떨어질 수 있습니다. 스위프트의 익스텐션을 정수형에 적용하여 서브스크립트로 정수를 구성하는 개별 바이트에 접근하는 방식은 어떨까요? 마치 복수의 바이트가 배열처럼 정수를 구성하는 것처럼 간주하는 것입니다.

Int32에 대한 서브스크립트를 추가하는 예입니다.

extension UInt32 {
    public subscript(index: Int) -> UInt32 {
        get {
            precondition(index<4,"Byte set index out of range")
            return (self & (0xFF << (index.toU32*8))) >> (index.toU32*8)
        }
        set(newValue) {
            precondition(index<4,"Byte set index out of range")
            self = (self & ~(0xFF << (index.toU32*8))) | (newValue << (index.toU32*8))
        }
    }
}

var i32:UInt32=982245678                        //HEX: 3A8BE12E

print(String(i32,radix:16,uppercase:true))      // Printing the hex value

i32[3] = i32[0]
i32[1] = 0xFF
i32[0] = i32[2]

print(String(i32,radix:16,uppercase:true))      //HEX: 2E8BFF8B

마법의 XOR

다른 프로그래밍 언어를 이미 알고 있는 사람이라면 비트 스트림을 키 값으로 XOR 연산을해서 간단히 암호화하는 방법에 대해서 알고 있을 것입니다. 간단히 메시지와 동일한 크기의 키를 사용하는 예를 들어 보겠습니다.

let secretMessage = 0b10101000111110010010101100001111000 // 0x547C95878
let secretKey =  0b10101010101010000000001111111111010    // 0x555401FFA
let result = secretMessage ^ secretKey                    // 0x12894782

let original = result ^ secretKey                         // 0x547C95878
print(String(original,radix:16,uppercase:true))           // 헥사값을 출력합니다.

XOR의 용법으로 자주 사용되는 예가 아래처럼 추가적인 임시 변수 없이 두 변수의 값을 바꾸는 방법입니다.

var x=1
var y=2
x = x ^ y
y = y ^ x   // y is now 1
x = x ^ y   // x is now 2

이중 부정

비트마스크를 사용해서 입력 비트 시퀀스에서 특정 비트가 설정되어 있는지 확인하는 방법으로 C/C++과 같은 언어에서는 이중 부정을 사용할 수 있었습니다. 이런 언어에서는 불린 값이 단순히 0과 0이 아닌 값으로 표현되는 정수형 값이기 때문에 가능하였습니다. 하지만 스위프트에서는 부정 논리연산자 !는 불린형 피연산자에만 적용할 수 있으므로 아래와 같은 코드는 불가능 합니다.

let input:UInt8 = 0b10101101
let mask:UInt8 = 0b00001000
let isSet = !!(input & mask)  // 만약 4번째 비트가 설정되어 있다면 이 값은 1이 될것입니다.
                              // 하지만 스위프트에서는 이런 문법이 허용되지 않습니다.

실제 프로그래밍에서 유용할지 모르겠으나 언어의 학습 측면에서 UInt8 정수에 이중 부정 논리연산을 수행하는 커스텀 연산자를 정의해 보겠습니다.

prefix operator ~~ {}

prefix func ~~(value: UInt8)->UInt8{
    return (value>0) ? 1 : 0
}

~~7  // 1
~~0  // 0

let isSet = ~~(input & mask)   // 1 as expected 

Bitter : 비트 처리를 위한 라이브러리

fig

여기서 설명한 여러가지 비트 처리 방법들은 모두 Bitter에 포함되어 있는 내용입니다. Bitter는 스위프트에서 비트 단위를 처리하기위한 여러가지 기능들을 “스위프트적인” 인터페이스로 제공하는 라이브러리입니다.

아직도 계속 개발되고 개선되고 있는 라이브러리이므로 한 번 둘러 보고 알아 두면 언제든 필요한 경우에 손 쉽게 사용할 수 있을 것입니다.