Skip to main content

ビット演算 — フラグをビットで持つ

ファイルの権限 755、TCP のフラグ、正規表現のオプション。これらはどれも複数の真偽値を 1 つの整数に詰め込んだものです。詰め込みと取り出しを担うのがビット演算です。

日常の Web 開発で毎日書くものではありませんが、既存のコードやプロトコルの仕様を読むときに必ず出てきます。

この章で学ぶこと

  • ビット演算子それぞれが何をするか
  • 複数のフラグを 1 つの整数で管理する方法
  • フラグの判定・追加・削除・反転の定型
  • JavaScript のビット演算が 32 ビット符号付きで動くこと
前提知識

2 進数の読み書きができると理解が早くなります。本章の中でも簡単に触れます。

整数はビットの並び

コンピュータは整数を 2 進数で持ちます。8 ビットなら次のような対応です。

10 進2 進 (8 ビット)
100000001
200000010
400000100
500000101
800001000

右端が 1 の位、その左が 2 の位、次が 4 の位と、左へ行くほど 2 倍になります。桁の位置を「ビット 0」「ビット 1」と右から数えるのが慣例です。5 は 00000101 なので、ビット 0 とビット 2 が立っています。

演算子

& — AND (論理積)

両方のビットが 1 のときだけ 1 になります。

00000101 (5)
& 00000011 (3)
------------
00000001 (1)

特定のビットが立っているかを調べるのに使います。

| — OR (論理和)

どちらかが 1 なら 1 になります。ビットを立てるのに使います。

00000101 (5)
| 00000010 (2)
------------
00000111 (7)

^ — XOR (排他的論理和)

片方だけが 1 のときに 1 になります。ビットを反転させるのに使います。同じ値で 2 回 XOR すると元に戻る性質があります。

00000101 (5)
^ 00000110 (6)
------------
00000011 (3)

~ — NOT (ビット反転)

すべてのビットを反転します。ビットを落とすためのマスクを作るのに使います。

<<>> — シフト

ビットの並びを左右にずらします。

1 << 0 → 00000001 (1)
1 << 1 → 00000010 (2)
1 << 2 → 00000100 (4)
1 << 3 → 00001000 (8)

**1 << n は「ビット n だけが立った値」**です。この形はビットフラグの基本になります。左シフトは 2 倍、右シフトは 2 で割る (小数切り捨て) のと同じ効果があります。

ビットフラグ

複数の真偽値を 1 つの整数で持つ手法です。それぞれの真偽値に別のビットを割り当てます。

const Permission = {
Read: 1 << 0, // 1 = 00000001
Write: 1 << 1, // 2 = 00000010
Delete: 1 << 2, // 4 = 00000100
Admin: 1 << 3, // 8 = 00001000
} as const;

値が 2 のべき乗になっているのがポイントです。それぞれが違う桁を使うので、足し合わせても情報が混ざりません。

判定する

/** value に flagBit が立っているか */
function hasFlag(value: number, flagBit: number): boolean {
return (value & flagBit) !== 0;
}

& で対象のビットだけを残し、0 でなければ立っています。

value = 5 (00000101) で試します。

呼び出し計算結果
hasFlag(5, Permission.Read)00000101 & 00000001 = 00000001true
hasFlag(5, Permission.Write)00000101 & 00000010 = 00000000false
hasFlag(5, Permission.Delete)00000101 & 00000100 = 00000100true

5 は「読み取りと削除は可、書き込みは不可」を表しています。

ビット番号を受け取る形で書かれていることもあります。中身は同じです。

/** value のビット位置 position が立っているか */
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}

追加・削除・反転

操作書き方仕組み
立てるvalue | flagBitOR は 1 を残す
落とすvalue & ~flagBit対象だけ 0 のマスクと AND する
反転するvalue ^ flagBitXOR は片方だけ 1 なら 1
複数まとめるflagA | flagB別々の桁なので混ざらない
let perms = Permission.Read | Permission.Write; // 3 = 00000011

perms = perms | Permission.Delete; // 7 = 00000111 (削除を許可)
perms = perms & ~Permission.Write; // 5 = 00000101 (書き込みを剥奪)
perms = perms ^ Permission.Admin; // 13 = 00001101 (管理者を反転)

hasFlag(perms, Permission.Write); // false

「落とす」で ~ を使うのが定型です。~Permission.Write は「Write のビットだけ 0 で、他はすべて 1」というマスクになるので、AND を取ると対象だけが消えます。

マスクで一部を取り出す

連続した複数ビットをまとめて取り出したいときは、その幅ぶんのマスクを作ります。

// 下位 4 ビットだけを取り出す
const lower4 = value & 0b1111; // 0b1111 は 15

// 上位側のビット 4 から 7 を取り出して右へ寄せる
const upper4 = (value >> 4) & 0b1111;

(1 << width) - 1 で「幅 width のマスク」が作れます。1 << 400010000 で、1 を引くと 00001111 になります。

何のために使うのか

用途
権限・オプションの集合ファイルパーミッション、機能フラグ
プロトコルのヘッダーTCP のフラグ、IP のヘッダーフィールド
状態の集合を省メモリで持つ大量のレコードの真偽値属性
高速な集合演算和集合は OR、積集合は AND で 1 命令

真偽値を個別のカラムやプロパティで持つより、まとめて 1 つの整数で扱えるので比較や保存が軽くなります。複数条件の一致判定が 1 回の AND で済むのも利点です。

一方で可読性は落ちます。perms & 6 と書かれても意味は分かりません。定数に名前を付ける、判定を関数にまとめる、といった手当てが要ります。真偽値が数個で性能要件も無いなら、データ構造の選び方で見た Set やオブジェクトで持つほうが読みやすくなります。

JavaScript での注意点

JavaScript と TypeScript のビット演算には、他の言語と違う癖があります。

**オペランドは 32 ビット符号付き整数に変換されます。**MDN は「両方のオペランドを数値に変換したうえで、32 ビット整数へ変換してビット演算する」と記述しており、結果も 32 ビット整数です。表現は 2 の補数なので、扱える範囲は -2147483648 から 2147483647 です。

ここから 2 つの帰結があります。

// 1. 32 ビットを超える桁は捨てられる
const big = 2 ** 33; // 8589934592
console.log(big | 0); // 0 ← 上位ビットが失われる

// 2. ビット 31 を立てると負の数になる
console.log(1 << 31); // -2147483648
console.log(1 << 32); // 1 ← シフト量は 32 で一周する

**ビット 31 以上を使うフラグは素直に書けません。**33 個以上のフラグが要るなら BigInt を使うか、複数の数値に分けます。BigInt どうしのビット演算は 32 ビットへの切り詰めが起きません。

const flag40 = 1n << 40n; // BigInt なら桁あふれしない

なお、数値の型が uint8uint32 のように固定されている言語では、範囲を超えた分の扱い (切り捨て、あふれ) が言語ごとに決まっています。他言語のビット演算コードを読むときは、まずその型の幅と符号の有無を確認してください。

よくある誤解

1 << 3 は 3」8 です。1 << n は「ビット n だけが立った値」で、2 の n 乗になります。

「フラグの値は連番でよい」 — 2 のべき乗 (1・2・4・8) でなければ桁が重なって混ざります。300000011 なのでビット 0 とビット 1 の両方を占めます。

「JavaScript のビット演算は 64 ビットで動く」 — 32 ビット符号付きに変換されます。数値自体は 64 ビット浮動小数点ですが、ビット演算の前に切り詰められます。

「ビットフラグは常に効率的」 — 省メモリと高速判定と引き換えに可読性を失います。数個の真偽値ならオブジェクトのほうが適切です。

確認問題

問 1. 次のコードの出力を答えてください。
function isBitSet(value: number, position: number): boolean {
return (value & (1 << position)) !== 0;
}

const value = 10;

console.log(isBitSet(value, 0));
console.log(isBitSet(value, 1));
console.log(isBitSet(value, 3));

答え: 上から順に false / true / true

10 を 2 進数にすると 00001010 です。立っているのはビット 1 とビット 3 です。

呼び出し1 << positionAND の結果出力
isBitSet(10, 0)0000000100000000 = 0false
isBitSet(10, 1)0000001000000010 = 2true
isBitSet(10, 3)0000100000001000 = 8true

10 = 8 + 2 なので、ビット 3 (8 の位) とビット 1 (2 の位) が立っている、と分解して考えると暗算できます。

問 2. 権限から「書き込み」だけを剥奪したいです。正しいのはどれですか。
const Write = 1 << 1; // 2

// A
perms = perms - Write;
// B
perms = perms & ~Write;
// C
perms = perms ^ Write;
// D
perms = perms | ~Write;

答え: B

  • A (- Write) — もともと書き込み権が無い場合に壊れます。perms = 5 (00000101) から 2 を引くと 3 (00000011) になり、立っていなかったビット 1 が立ってしまいます
  • B (& ~Write)~Write は「ビット 1 だけ 0、他はすべて 1」のマスクです。AND を取ると対象だけが消え、他は保たれます。元から立っていなくても結果は変わりません
  • C (^ Write) — 反転なので、立っていなければ立ててしまいます。トグルであって剥奪ではありません
  • D (| ~Write) — OR なので、ビット 1 以外がすべて立ちます。全権限付与に近い結果になります

「落とすときは & ~」が定型です。引き算で書くと状態に依存して壊れます。

問 3. 40 個のフラグを 1 つの数値で管理しようとしたら、うまく動きませんでした。なぜですか。

答え: JavaScript のビット演算が 32 ビット符号付き整数で行われるためです。

1 << 40 は期待どおりの値になりません。シフト量は 32 で一周するので 1 << 401 << 8 と同じ 256 になります。ビット 32 以上は表現できません。

さらにビット 31 を使った時点で結果が負の数になります (1 << 31-2147483648)。

対策は 2 つあります。

  1. BigInt を使う1n << 40n のように書けば切り詰めが起きません。ただし通常の数値と混ぜると TypeError になります
  2. 複数の数値に分ける — 32 個ずつのグループに分割して配列で持ちます

そもそもフラグが 40 個ある設計自体を見直す価値もあります。この規模なら Set やオブジェクトのほうが読みやすく、保守しやすくなります。

まとめ

  • & は判定、| は追加、^ は反転、~ はマスク作りに使います
  • **1 << n は「ビット n だけが立った値」**で、ビットフラグの基本形です
  • フラグの値は 2 のべき乗にします。連番にすると桁が重なります
  • 立てるのは | flag、落とすのは & ~flag、反転は ^ flag が定型です。引き算で落とすと状態に依存して壊れます
  • **JavaScript のビット演算は 32 ビット符号付きに変換されます。**ビット 31 以上は素直に扱えません
  • 省メモリと高速判定の代わりに可読性を失います。数個の真偽値なら普通のオブジェクトが適切です
関連リファレンス

次に読む