2進数の桁は、結局のところ、CPUやRAMのトランジスタが「オン」か「オフ」かという状態にそのまま対応しています。
こうした低レベルな操作は、くだけた言い方では「ビット操作」と呼ばれ、特にシステムプログラミング言語で重要です。
Juliaのような高水準言語では、こうした詳細のほとんどは通常抽象化されています。しかし、言語本体にはビット単位の演算が一通り用意されています。
注: REPLで人間が読める形の2進数の出力を見るには、以下の例のほぼすべてを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"
左シフトするたびに値は2倍になり、右シフトするたびに値は半分になります(切り捨てを伴います)。これは10進数で表すと、よりはっきりわかります。
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で埋めます。
以前のコンセプトで、演算子&&(論理積)、||(論理和)、!(否定)は真偽値に対して使うことを学びました。
2つの整数のビットを比較する、同じ働きをする演算子として、&(ビット単位の論理積)、|(ビット単位の論理和)、~(チルダ、ビット単位の否定)があります。
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