Skip to the content.
2024/04/16

Programming Tips: ビット演算

導入

論理演算

0 と 1 のいずれかの値を取る変数、すなわち1ビットの変数 \(x, y\) に対し、 演算 \(\lnot x\), \(x \land y\), \(x \lor y\), \(x \oplus y\) を 否定 (not) 、論理積 (and) 、論理和 (or) 、排他的論理和 (xor) と呼び、 以下のように定義される。

否定 (not)

\(x\) \(\lnot x\)
0 1
1 0

論理積 (and)

\(x\) \(y\) \(x \land y\)
0 0 0
0 1 0
1 0 0
1 1 1

論理和 (or)

\(x\) \(y\) \(x \lor y\)
0 0 0
0 1 1
1 0 1
1 1 1

排他的論理和 (exclusive or - xor)

\(x\) \(y\) \(x \oplus y\)
0 0 0
0 1 1
1 0 1
1 1 0

ビット演算 (Bitwise Operators)

以下に例を示す(変数はわかりやすく 8 ビットとしよう)。

bitwise not

  二進数 16進数 10進数
x 00001111 0x0f 15
~x 11110000 0xf0 240

bitwise and

  二進数 16進数 10進数
x 01010101 0x55 85
y 00001111 0x0f 15
x & y 00000101 0x05 5

この演算では、x and 0 = 0, x and 1 = x になることから、 ビット列中の特定のビットだけ注目したい時に役立つ。

この例では、00001111 と and を取っているので、 x の下位4ビットである 0101 がそのまま抽出され 上位4ビットはすべて 0 になる。このような操作をマスクとも呼ぶ。

bitwise or

  二進数 16進数 10進数
x 01010101 0x55 85
y 00001111 0x07 7
x| y 01011111 0x5f 95

この演算では、x or 0 = x, x or 1 = 1 になることから、 特定のビットを 1 にし、その他のビットは変化させない、 という処理を行うことができる。

この例では、上位4ビットは 0101 のまま変化せず、 下位4ビットを 1111 に変化させた。

bitwise xor

  二進数 16進数 10進数
x 01010101 0x55 85
y 00001111 0x07 7
x ^ y 01011010 0x5a 90

この演算では、y で指定したビットパターンに基づいて、 x のビットを反転させることができる。 y = 00001111 なので、 x の上位4ビットはそのまま変化させず、 下位4ビットだけ 0と1 を反転させる、という操作になる。

基本編

これらを組み合わせて、1つの変数に複数のフラグをまとめることもできる。 例として、RPG を作ることを考えよう。 キャラクターの状態異常を、

enum BadStatus {
    PARALYSIS = 1,
    POISON = 2,
    SLEEP = 4,
    STUN = 8,
}

と定義する。例えば、スタンと麻痺を同時に受けているときは、 それぞれの数値を足し合わせた 9 になる。さて、変数 status に対して

if ( status & BadStatus::PARALYSIS ) {
   //  麻痺している  //
}
if ( status & BadStatus::POISON ) {
   //  毒を受けている  //
}
...

とすることで、各状態異常の判定ができる。

同様に、敵の攻撃を受けて毒の状態異常が付与された時は、

status = status | BadStatus::POISON;

と書くことができる。この時、 変数は各状態異常を示す定数の和だからと言って、間違っても

status = status + BadStatus::POISON;

と書いてはいけない。これでは、 すでに毒を受けている状態で、さらに毒を受けると (数値が 4 になるから)なぜか毒が解除されて眠ってしまう。

if ( status & BadStatus.POISON == 0 ) {
    status = status + BadStatus::POISON;
}

と判定してもよいが、元の値が 0 でも 1 でも目的のビットだけ 1 (他はそのまま) にできる or を使うのが単純だし、コードの意図も分かりやすい。

同様に、解毒魔法を使ったときは、

status = status & (~ BadStatus::POISON);

でよい。この場合もうっかり引き算してしまうと、 毒を受けていないキャラクタに解毒魔法を使うと逆に毒を受けてしまう (最悪、繰り下がって、眠りとスタンも受ける)というバグが発生する。

おまけ

先の例では、各状態に1ビットずつ使ったが、 特定のビットは数ビットで1要素としてもよい。 例えば下位2ビットに戦闘から逃げるのに失敗した回数、 残りのビットが1ビット1フラグ、 等である。 この場合は、逃げた回数の増加の際には

flag = (flag + 1) & 0x03 | (flag & 0xfc)

のようにきちんとマスクをしないと、余計なビットに繰り上がるバグが発生する。 実際、ファミコン時代の某有名なゲームでこの致命的なミスがある。

そのゲームでは、

というフラグと、戦闘から逃げた回数 (E) を、 1つの変数に管理していたと思われる。 裏技サイトなどを参考に推測すると、以下のようなビット列と思われる

bit 7 6 5 4 3 2 1…0
意味 (D) ??? ??? ??? (C) (T) (E)

(??? の部分は不明)

したがって、戦闘から逃げるのに8回失敗すると、 味方の攻撃がすべてクリティカルヒットになるという裏技がある (初期出荷版に限る)。

実際には4回目には必ず逃げられる仕様と思われるが、 ボス戦では逃げられないので、このバグにより、ラスボスをボコれる。

ちなみに、裏技サイトによると、 先に (T) のアイテムを使っておけば、8回ではなく4回で済むし、 128回逃げるのに失敗すると、突然魔法が使えなくなる。(らしい)。

応用編

剰余

考えやすくするため、まず 10 進数の場合

2 進数の場合

この考え方は、2 進数の場合にも適用できる。

さて、導入で説明した通り 抽出したいビットを 1 それ以外を 0 にしたビットパターンと bitwise and 特定のビットだけ取り出せるのであった。

したがって、上の各例は

のようにあらわすことができる。

すなわち、除数が \(d = 2 ^ n\) の場合に限り、 x を d で割った余りを求める公式は、

x & (d - 1)

である。例えば x を 16 で割った余りは

x & 15

となる(除数は 16 だが & を使うときは 15 になるので注意)。

切り捨て

切り捨ては、剰余とは逆に、 下位ビットを 0 で埋めてしまえばよい。

これを実現するには、上位ビットを 1、下位3ビットを 0 とした 11111000 と bitwise and をとり、 00100011 & 11111000 = 00100000 (35 & 247 = 32) となる

なお、11111000 というパターンを作るには、bitwise not を使えば 11111000 = ~ 00000111 である。

したがって、\(d = 2 ^n\) の場合に限り、 ある数 x を d の倍数に切り捨てる公式は

x & ~(d - 1)

である。例えば 16 の倍数に切り捨てるなら

x & ~15

となる。

切り上げ

切り上げは、適切な定数を加えてから切り捨てればよい。

これは 2 進数でも同様で、\(d = 2^n\) の場合に限り、 ある数 x を d の倍数に切り上げる公式は、

(x + d - 1) & ~(d - 1)

となる。例えば 16 の倍数に切り上げるなら

(x + 15) & ~15

である。

tags: