基礎●●○○○

ビット演算

ビットえんざん

数値をビットの列とみなして行うAND・OR・XOR・シフトなどの演算。低レイヤ処理の道具。

概要

ビット演算は、数値を「数」としてではなく「2進数のビットの列」とみなし、桁ごとに直接操作する演算です。代表的なものは AND(両方1なら1)、OR(どちらかが1なら1)、XOR(どちらか一方だけが1なら1)、NOT(0と1を反転)、そしてビット列全体を左右にずらすシフト演算です。多くの言語で & | ^ ~ << >> という演算子が割り当てられています。

足し算や掛け算が「値」を計算するのに対し、ビット演算は「特定の桁を取り出す・立てる・消す・反転する」という桁単位の外科手術を行います。日常のアプリケーション開発で毎日書くものではありませんが、ネットワークのIPアドレス計算、権限フラグ、ファイルフォーマットの解析、ハッシュ関数や暗号の内部など、低レイヤに近づくほど必ず顔を出す基本の道具です。

なぜ生まれたか

ビット演算はソフトウェアの発明ではなく、ハードウェアの姿がそのまま言語に露出したものです。CPUの演算回路は、AND・OR・NOTといった論理ゲートの組み合わせでできており、ビット単位の論理演算は加算よりもさらに素朴な、回路が最も自然に実行できる操作です。初期のプログラミング言語がこれらを演算子として提供したのは、機械の能力を直接使わせるためでした。

もうひとつの動機はメモリの節約です。メモリが極端に高価だった時代、「はい/いいえ」の情報1つに1バイト(8ビット)を使うのは贅沢でした。1バイトに8個のフラグを詰め込み、ビット演算で個別に読み書きする — この「ビットを主語にした設計」は、メモリが潤沢になった現在でも、ネットワークプロトコルのヘッダやハードウェア制御のレジスタなど、1ビットを無駄にできない世界の共通言語として生き続けています。

詳細

4つの論理演算 — 桁ごとに独立して働く

ビット演算の第一の特徴は、各桁が隣の桁と無関係に、独立して計算されることです。足し算のような繰り上がりはありません。同じ入力 1100 と 1010 に対して、AND・OR・XOR がそれぞれどう働くかを並べると、性格の違いがよく見えます。

AND 両方1なら1OR どちらかが1なら1XOR 片方だけ1なら11100101010001100 & 1010 = 10001100101011101100 | 1010 = 11101100101001101100 ^ 1010 = 0110上段と中段が入力、下段が結果。金色の枠が1のビット
同じ入力 1100 と 1010 に対する AND・OR・XOR の結果 — 各桁は独立に計算される

NOT(~)は1つの値の全ビットを反転します。シフト演算(<< >>)はビット列全体を左右にずらす操作で、左に1ビットずらすと値は2倍、右にずらすと2分の1(切り捨て)になります。10進数で桁をずらすと10倍・10分の1になるのと同じ理屈です。

実際にビットを自分で立てたり消したりして、結果の桁がどう決まるかを確かめてみましょう(プリセットには、この後のセクションで登場する実例も入っています)。

⚡ 体験: ビット演算ラボ
A204 0xCC
B170 0xAA
A & B
=10001000136 0x88
実例:

ビットをクリックして0/1を切り替え、結果の桁がどう決まるかを観察してみてください。桁にカーソルを載せる(タップする)と対応が見えます。

ビットマスク — 欲しい桁だけを扱う技法

ビット演算の実務パターンの中心が「マスク」です。取り出したい桁だけを1にした値(マスク)を用意し、AND を取れば他の桁が消えて目的の桁だけが残ります。逆に OR でマスクを重ねれば特定のビットを立てられ、XOR なら反転できます。「AND は切り抜き、OR は上書きで立てる、XOR はトグルスイッチ」と覚えると使い分けに迷いません。

この技法の代表例がフラグ管理です。Unix のファイル権限 755 は、読み・書き・実行の3ビットを所有者・グループ・その他の3人分並べた9ビットの値(111 101 101)で、「書き込み権があるか」は該当ビットとの AND ひとつで判定できます。GUIツールキットのオプション指定やゲームの状態管理などで見かける FLAG_A | FLAG_B という書き方も、複数のフラグを1つの整数に OR で合成しているのです。

実例 — サブネットマスクはその名の通りマスク

IPアドレスの「サブネットマスク」は、ビットマスクがネットワークの根幹で使われている好例です。IPv4 アドレスは32ビットの値で、その前半が「どのネットワークか」、後半が「ネットワーク内のどの機器か」を表します。サブネットマスク(例: 255.255.255.0 = 上位24ビットがすべて1)とアドレスの AND を取ると、機器部分のビットが消えてネットワークアドレスだけが残ります。ルータが「この宛先は同じネットワークか、外へ転送すべきか」を判断するたびに、この AND 演算が実行されています。

XOR の面白い性質 — 可逆性と暗号

XOR には他の演算にない際立った性質があります。同じ値で2回 XOR すると元に戻る(x ^ k ^ k = x)、つまり演算そのものが可逆なのです。AND や OR は情報を潰してしまう(結果から入力を復元できない)のに対し、XOR は鍵さえあれば巻き戻せます。

この性質は暗号化の心臓部で使われています。平文と鍵ストリームの XOR で暗号化し、同じ鍵ストリームとの XOR で復号する — ストリーム暗号の基本形はまさにこれです。また「x ^ x = 0 になる」性質は、データの誤り検出(パリティ)や、RAID のパリティによる復元、ハッシュ関数の内部の攪拌処理などにも応用されています。

落とし穴

実務でつまずきやすいのは、まず論理演算子との混同です。多くの言語で &(ビット AND)と &&(論理 AND)は別物で、if (flags & MASK && other) のような式は演算子の優先順位も絡んで意図しない結果になりがちです。ビット演算は比較演算子より優先順位が低い言語が多く、flags & MASK == MASKflags & (MASK == MASK) と解釈される事故は古典的なバグです。括弧を惜しまないのが鉄則です。

また、符号付き整数の右シフトは言語によって「符号ビットを引き延ばす(算術シフト)」か「0で埋める(論理シフト)」かが異なり、負の数で挙動が分かれます。JavaScript に >>>>> の2種類があるのはこのためです。ビット演算を使うときは、対象を符号なし整数に揃えるか、言語仕様を確認してから使うのが安全です。