ざっくりZig - 2進数とビット演算

2024/03/11に公開

プログラミングでは、一般的な四則演算(+, -, *, /)や関数以外にビット演算という独特の計算方法があります。これは、2進数のある桁だけを操作したり、ビットを左右にずらしたりするものです。

2進数とビット

2進数は1つの桁に0か1しかない数値の表し方です。これはコンピュータ内部の数値表現を数字に置き換えたものです。2進数の1桁のことを ビット といい、実際の数値は複数のビットを並べて表現します。
2進数では、左の1は右の1を2倍した数になります。例えば、2進数の1は10進数と同じ1ですが、2進数の10は1を2倍した2を表します。そして2進数の100は2を2倍した4、2進数の1000は4を2倍した8を表します。

2進数 10進数
1 1
10 2
100 4
1000 8
11 2 + 1 = 3
110 4 + 2 + 0 = 6
1011 8 + 0 + 2 + 1 = 11

ただ、2進数は数値が大きくなるとあっという間に桁数が増えてしまいます。そのため、大きな数を表すときは、より少ない桁数で表せる8進数や16進数が使われます。8進数は2進数の下位(右から)3桁ずつまとめて0~7(111)で表したもので、16進数は2進数の同じく下位4桁ずつをまとめて0~F(1111)で表したものです。16進数では10-15までをA-Fあるいはa-fで表します。

10進数 2進数 8進数 16進数
0 0 0 0
1 1 1 1
2 10 2 2
3 11 3 3
4 100 4 4
5 101 5 5
6 110 6 6
7 111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 A
11 1011 13 B
12 1100 14 C
13 1101 15 D
14 1110 16 E
15 1111 17 F

ビット演算

ビット演算は特定のビットを変更する方法で、元のビットを反転させたり、あるいは0にしたり、1にしたりします。ここでは&, |, ^という3つの演算子を紹介します。(あと1つは別途紹介します)

a & b (AND) - 論理積

2つの数値で同じ位置にあるビットが両方とも1のときだけ、結果は1となり、それ以外はすべて0となります。そのため、次の表のaが0でも1でもbが0なら結果は0となります。これを利用して特定のビットを0にするときによく用いられます。

a b a & b
0 0 0
0 1 0
1 0 0
1 1 1
// {b:0>3} - b: 2進数, 0>: 左余白に0詰め, 3: 表示領域が3桁
try stdout.print("0b{b:0>3} & 0b{b:0>3} = 0b{b:0>3}\n", .{ 6, 2, 6 & 2 });
try stdout.print("0b{b:0>3} & 0b{b:0>3} = 0b{b:0>3}\n", .{ 5, 6, 5 & 6 });

結果
0b110 & 0b010 = 0b010     # 2ビット目以外を0にする
0b101 & 0b110 = 0b100     # 最下位ビットを0にする

a | b (OR) - 論理和

2つの数値で同じ位置にあるビットのどちらかが1のとき結果は1となり、両方が0のときだけ0となります。そのため、次の表のaが0でも1でもbが1なら結果は1となります。これを利用して特定のビットを1にするときによく用いられます。

a b a | b
0 0 0
0 1 1
1 0 1
1 1 1
try stdout.print("0b{b:0>3} | 0b{b:0>3} = 0b{b:0>3}\n", .{ 2, 3, 2 | 3 });
try stdout.print("0b{b:0>3} | 0b{b:0>3} = 0b{b:0>3}\n", .{ 5, 2, 5 | 2 });

結果
0b010 | 0b011 = 0b011   # 最下位ビットと2ビット目を1にする
0b101 | 0b010 = 0b111   # 2ビット目を1にする

a ^ b (XOR) - 排他的論理和

2つの数値の同じ位置にあるビットの値が異なるとき結果は1となり、それが同じなら結果は0となります。そのため、aの値が0でも1でもbが1のとき結果はaの値を反転したものになります。また、同じ値を^で演算すると結果は0になります。

a b a ^ b
0 0 0
0 1 1
1 0 1
1 1 0
try stdout.print("0b{b:0>3} ^ 0b{b:0>3} = 0b{b:0>3}\n", .{ 5, 7, 5 ^ 7 });
try stdout.print("0b{b:0>3} ^ 0b{b:0>3} = 0b{b:0>3}\n", .{ 2, 7, 2 ^ 7 });
try stdout.print("0b{b:0>3} ^ 0b{b:0>3} = 0b{b:0>3}\n", .{ 5, 5, 5 ^ 5 });

結果
0b101 ^ 0b111 = 0b010   # ビットを反転
0b010 ^ 0b111 = 0b101   # ビットをさらに反転(元に戻る)
0b101 ^ 0b101 = 0b000   # 同じ値を演算すると結果は0

ビットシフト

ビットシフトは桁を左右にずらすもので、1ビット左シフト(左に1つずらす)すると値が2倍され、1ビット右シフト(右に1つずらす)すると値が約半分(1/2)になります。左シフトの演算子は << 、右シフトの演算子は >> で表します。演算子の左にシフトする数、右にシフトする回数を書きます。ただし、シフトする回数はコンパイル時点で確定していなくてはなりません。

左シフトすると右側(下位ビット)に0が追加されます。

// {b:0>8} - b: 2進数, 0>: 左余白に0詰め, 8: 表示領域が3桁
// {d:3} - d: 10進数, 3: 表示領域が3桁
try stdout.print("0b{b:0>8} << 0 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 0, 1 << 0 });
try stdout.print("0b{b:0>8} << 1 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 1, 1 << 1 });
try stdout.print("0b{b:0>8} << 2 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 2, 1 << 2 });
try stdout.print("0b{b:0>8} << 3 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 3, 1 << 3 });
try stdout.print("0b{b:0>8} << 4 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 4, 1 << 4 });
try stdout.print("0b{b:0>8} << 5 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 5, 1 << 5 });
try stdout.print("0b{b:0>8} << 6 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 6, 1 << 6 });
try stdout.print("0b{b:0>8} << 7 = 0b{b:0>8} / {d:3}\n", .{ 1, 1 << 7, 1 << 7 });

結果
0b00000001 << 0 = 0b00000001 /   1
0b00000001 << 1 = 0b00000010 /   2
0b00000001 << 2 = 0b00000100 /   4
0b00000001 << 3 = 0b00001000 /   8
0b00000001 << 4 = 0b00010000 /  16
0b00000001 << 5 = 0b00100000 /  32
0b00000001 << 6 = 0b01000000 /  64
0b00000001 << 7 = 0b10000000 / 128

右シフトすると、左側(上位ビット)に0が追加されます。右側にあった1は右シフトによってなくなることがあります。

try stdout.print("0b{b:0>8} >> 0 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 0, 255 >> 0 });
try stdout.print("0b{b:0>8} >> 1 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 1, 255 >> 1 });
try stdout.print("0b{b:0>8} >> 2 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 2, 255 >> 2 });
try stdout.print("0b{b:0>8} >> 3 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 3, 255 >> 3 });
try stdout.print("0b{b:0>8} >> 4 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 4, 255 >> 4 });
try stdout.print("0b{b:0>8} >> 5 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 5, 255 >> 5 });
try stdout.print("0b{b:0>8} >> 6 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 6, 255 >> 6 });
try stdout.print("0b{b:0>8} >> 7 = 0b{b:0>8} / {d:3}\n", .{ 255, 255 >> 7, 255 >> 7 });

結果
0b11111111 >> 0 = 0b11111111 / 255
0b11111111 >> 1 = 0b01111111 / 127
0b11111111 >> 2 = 0b00111111 /  63
0b11111111 >> 3 = 0b00011111 /  31
0b11111111 >> 4 = 0b00001111 /  15
0b11111111 >> 5 = 0b00000111 /   7
0b11111111 >> 6 = 0b00000011 /   3
0b11111111 >> 7 = 0b00000001 /   1

まとめ

  • 2進数
    • コンピュータ内部の数値表現を0と1に置き換えたもの
    • 左の桁の1は右の桁の1の2倍 (1101 / 8 + 4 + 1 = 13)
  • ビット
    • 2進数の1桁(0か1)
    • 8進数は2進数の3桁をまとめて0-7で表現
    • 16進数は2進数の4桁をまとめて0-9, A-F(a-f)で表現
  • ビット演算
    • ビットの値を変更したり反転したりする
    • &: 論理積 - あるビットが両方とも1のとき1、それ以外は0
    • |: 論理和 - あるビットのどちらかが1のとき1、両方が0のとき0
    • ^: 排他的論理和 - あるビットがどちらも同じ値の時0、異なる時1
  • ビットシフト
    • ビットを左に1桁ずらす(<<)と2倍、右に1桁ずらす(>>)と半分
    • シフトする回数はコンピいる時点で確定していなくてはならない

ビット幅と演算 >
< 数値の表し方と計算
ざっくりZig 一覧

Discussion