ビット演算完全ガイド:AND・OR・XOR・シフトとビットマスクの実践
古い PostgreSQL のマイグレーションを開いたら permissions & 0b100 が出てきた。同僚が 32 個の真偽値を 1 つの整数に詰め込んだ feature flag システムをリリースした。Kubernetes のサブネット計算が 192.168.1.0/24 を返してきて、ネットワークアドレスをコードで抽出することになった。場面はバラバラでも、必要なのは同じ基盤スキル、ビット演算です。
アプリ層の開発者は Web アプリで & や ^ を書く機会がほとんどありません。書く日が来るまでは、ですが。本ガイドでは 6 つのビット演算子、2 の補数、覚えておくと便利な 9 つのパターン、そして言語ごとの落とし穴(特に JavaScript)を順に見ていきます。コードは JS、Python、Go、C の 4 言語で対照し、全例が実行可能です。
別タブで 進数変換ツール を開いておくとよいでしょう。いくつかの節で数値を入力してビットパターンの変化を確かめてもらいます。
なぜ 2026 年になってもビット演算を学ぶ価値があるのか
高級言語はビット演算を不要にしたわけではなく、見えにくくしただけです。気づかないうちに依存している場面をいくつか挙げます。
- PostgreSQL の行レベルセキュリティは ACL 権限(
SELECT、INSERT、UPDATE、DELETEなど)を 1 つの整数ビットマップに格納します。 - Linux capabilities は旧来の「root か普通ユーザーか」の二択モデルを 40 以上の権限ビットに置き換え、
|で合成します。 - JWT のアルゴリズムヘッダはハッシュ方式を小さなフィールドにエンコードし、ライブラリ層ではビット単位の比較が一般的です。
- Snowflake / ULID / UUIDv7 はタイムスタンプ、マシン ID、シーケンス番号を左シフトで 64 ビットや 128 ビットの整数にパックします。
- Redis の
BITCOUNT/BITOPは基数推定や A/B 分配のためにビット演算プリミティブをアプリ層へ直接公開します。 - 画像処理では 32 ビット RGBA ピクセルを読み、
&と>>でチャネルを抽出します。
ビット演算は CPU の命令レベルで O(1) のままです。32 個の真偽値を 1 つの整数に詰めれば 31 バイトのメモリが浮き、しかも「この 32 フラグのどれかが立っているか」を 1 回の != 0 比較で判定できます。
まず押さえる二進数の基礎
本ガイドは二進数の基本を理解している前提です。復習が必要なら、先に進数変換完全ガイドを読んでから戻ってきてください。
語彙を軽く整理しておきます。
- ビット:0 または 1
- ニブル:4 ビット(16 進 1 桁に対応)
- バイト:8 ビット
- ワード:CPU に応じて通常 32 または 64 ビット
多くの言語で整数は固定幅(8、16、32、64 ビット)です。ビット幅はビット演算で大きな意味を持ちます。シフトはビットを幅の外に押し出しますし、符号付き整数では左端に符号ビットが居座ります。
すぐに試したい人は、進数変換ツール を開いて 10 進で 170 と入力してみてください。二進出力に 10101010 が出るはずです。交互に並ぶこのパターンは後の節で何度か登場します。
6 つのビット演算子
主要言語はどれも同じ 6 つの演算子を備えており、構文差はごくわずかです。&、|、^、~、<<、>> は JavaScript、Python、Go、Rust、C、C++、Java、C# で同じ形。JavaScript はこれに加えて >>>(符号なし右シフト)を持っています。
AND(&)──ビットフィルタ
両方の入力ビットが 1 のときだけ出力が 1 になります。
| A | B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
AND はゲートのようなもので、両方で立っているビットだけが通過します。代表的な用途はマスキング、つまり一部のビットを残して他をゼロにする操作です。
// 下位 4 ビット(右端のニブル)を抽出
const value = 0b11010110; // 214
const low4 = value & 0x0F; // 0b00000110 = 6
// 奇数判定
const isOdd = (n) => (n & 1) === 1;
isOdd(7); // true
isOdd(42); // false
# Python も同じ
value = 0b11010110
low4 = value & 0x0F # 6
def is_odd(n):
return (n & 1) == 1
OR(|)──ビットセッター
入力ビットのどちらかが 1 なら出力が 1 になります。
| A | B | A | B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
OR はフラグをまとめるのに使います。READ = 1、WRITE = 2、EXECUTE = 4 なら READ | WRITE は 3 で、両方の権限が有効になります。
const READ = 0b001;
const WRITE = 0b010;
const EXEC = 0b100;
const rw = READ | WRITE; // 0b011 = 3
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE # 3
XOR(^)──ビットトグル
入力ビットが異なるときに出力が 1 になります。
| A | B | A ^ B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
XOR には、コンピュータサイエンスでも切れ味のあるテクニックを支える 3 つの代数的性質があります。
a ^ a = 0自分自身と XOR すると消えるa ^ 0 = aゼロとの XOR は恒等a ^ b ^ a = bXOR は自分自身の逆演算
最後の性質があるからこそ、XOR はパリティチェックやストリーム暗号、さらには「全要素が偶数回現れる中で唯一奇数回現れる値を見つける」という面接題で頻出するわけです。
// 他の要素は 2 回ずつ現れ、1 つだけ 1 回しか現れない配列から唯一の値を探す
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]); // 4
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2]) # 4
NOT(~)──ビット反転
単項の ~ は全ビットを反転します。0 は 1 に、1 は 0 に変わります。
~0b00001111 // -16(JavaScript は 32 ビット符号付きに強制)
~5 // -6
~5 # -6
// Go では単項の ^ がビット NOT──記号が違う点に注意
var x int8 = 5
fmt.Println(^x) // -6
~5 = -6 という結果は主要言語のほとんどで共通で、初見だと戸惑うところです。理由は 2 の補数表現にあり、次の節で掘り下げます。とりあえず覚えておきたいのは、負の数を 2 の補数で扱う言語(つまり主要言語すべて)では ~x が常に -(x + 1) になる、ということです。
左シフト(<<)──2 のべき乗による乗算
x << n は x の全ビットを左へ n 桁ずらし、右端にゼロを詰めます。数学的には 2ⁿ の乗算と同じです。
1 << 0 // 1 (2^0)
1 << 1 // 2 (2^1)
1 << 3 // 8 (2^3)
1 << 10 // 1024 (2^10 = 1 KiB)
// フラグの定義
const FLAG_ADMIN = 1 << 0;
const FLAG_EDITOR = 1 << 1;
const FLAG_REVIEWER = 1 << 2;
1 << n のいいところは、位置 n にちょうど 1 ビットだけ立った数を作れる点です。この 1 ビットがそのままフラグになります。
オーバーフローには注意してください。JavaScript では 1 << 31 は 2147483648 ではなく -2147483648 になります。ビット演算が 32 ビット符号付き整数で実行されるためです。
右シフト(>> と >>>)──符号付きかゼロ埋めか
右シフトはビットを右へずらします。問題は、左側に空いた位置に何を詰めるかです。
>>(算術右シフト):符号ビットを維持。負の数は負のまま。>>>(論理・符号なし右シフト):ゼロで埋める。専用演算子を持つのは JavaScript だけ。
-8 >> 1 // -4 (符号保持)
-8 >>> 1 // 2147483644 (符号ビットもデータとして扱う)
8 >> 1 // 4
8 >> 2 // 2
C では符号付き型の >> が算術か論理かは実装依存です。大半のコンパイラは算術右シフトを行いますが、確認せずに頼るのは危険です。Go はシフト量を符号なし整数に限り、符号付き / 符号なし型を明確に区別します。Python は固定幅を持たないので >>> は存在しません。
2 の補数──負の数を格納する仕組み
ビットが 0 と 1 しかないなら、-5 はどう表現するのか。1960 年代に世界が辿り着いた答えが 2 の補数(two’s complement)で、現代の CPU はすべてこれを採用しています。
素朴な方法、つまり 1 ビットを符号に使う方式には 2 つの欠点があります。まず +0 と -0 が両方存在して扱いづらい。次に加算・減算回路が符号ビットを確認しなければならず、ハードウェアが複雑になる。2 の補数はこれらを同時に片付けます。
ルールはシンプルです。
- 正の二進表現を書く
- 全ビットを反転(これが「1 の補数」)
- 1 を足す
8 ビット 2 の補数で -5 を符号化してみましょう。
5 の二進: 0000 0101
全ビット反転: 1111 1010 (これは 2 の補数で -6!)
1 を足す: 1111 1011 ← これが -5
進数変換ツールで確かめてみましょう。進数変換ツール に 10 進で 251 を入れると、二進出力は 11111011 になります。8 ビット符号付きの文脈では 11111011 は -5、同じビット列を 8 ビット符号なしで解釈すると 251。ビット列はまったく同じで、解釈だけが違うのです。
これで先ほどの ~5 = -6 の謎も解けます。ビット NOT はビットを反転して 1 の補数を作り、2 の補数は 1 の補数に 1 を足した値ですから、
~x = -(x + 1) // 2 の補数を使うどの言語でも成り立つ
~5 = -6
~(-3) = 2
n ビット符号付き整数の表現範囲は -2ⁿ⁻¹ から 2ⁿ⁻¹ − 1。8 ビット符号付きは -128 から 127、32 ビット符号付きはおよそ -21 億 から +21 億 です。
必修のビット操作 9 パターン
この 9 つで、書くことになるビット操作の 95% はカバーできます。覚えておくとシステムコードのあちこちで目が効くようになります。
ビットを立てる──x | (1 << n)
n ビット目を 1 にし、他はそのまま。
let flags = 0b0100;
flags = flags | (1 << 0); // 0b0101
ビットを落とす──x & ~(1 << n)
n ビット目を 0 にし、他はそのまま。~(1 << n) は n ビット目以外が全部 1 のマスクです。
let flags = 0b0111;
flags = flags & ~(1 << 1); // 0b0101
ビットを反転──x ^ (1 << n)
現状に関わらず n ビット目を反転します。
let flags = 0b0100;
flags = flags ^ (1 << 2); // 0b0000
flags = flags ^ (1 << 2); // 0b0100 に戻る
ビットをチェック──(x >> n) & 1
n ビット目が立っていれば 1、そうでなければ 0。同等:(x & (1 << n)) !== 0。
const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1; // 1
最下位の立ちビットを取り出す──x & -x
x の最右端の 1 だけが残る値を得ます。2 の補数では -x が ~x + 1 に等しく、最下位の立ちビットまでを反転する性質を利用したトリックです。
const x = 0b10110100;
const lowest = x & -x; // 0b00000100 = 4
これが Fenwick 木(Binary Indexed Tree)の O(log n) 累積和の核心です。
立ちビット数を数える(popcount)
整数中の 1 の個数を数えます。多くの言語に組み込み関数があります。
// JavaScript(手書き版)
const popcount = (n) => {
let count = 0;
while (n) { count += n & 1; n >>>= 1; }
return count;
};
popcount(0b10110100); // 4
# Python 3.10+
(0b10110100).bit_count() # 4
// Go
import "math/bits"
bits.OnesCount(0b10110100) // 4
一時変数なしの XOR スワップ
古典ネタとして、3 つ目の変数を使わず 2 整数を入れ替える方法があります。本番では使わないでください(一時変数より遅く、a と b が同じメモリを指すと壊れます)。ただ、仕組みは知っておくと面白いです。
let a = 5, b = 9;
a = a ^ b; // a = 5 ^ 9
b = a ^ b; // b = (5 ^ 9) ^ 9 = 5
a = a ^ b; // a = (5 ^ 9) ^ 5 = 9
// a = 9, b = 5
2 のべき乗判定──(x & (x - 1)) === 0
2 のべき乗はビットが 1 つだけ立っています。1 を引くとそのビットが消えて、下位ビットがすべて立ちます。AND の結果がゼロになるのは 2 のべき乗(と 0)だけ。0 を弾くために x > 0 を付けます。
const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16); // true
isPow2(17); // false
高速な奇偶判定──x & 1
言語によっては x % 2 より速く、コンパイラの最適化が効けば同等になります。ホットループや、明示的にビットで考えたい場面で役立ちます。
const isOdd = (x) => (x & 1) === 1;
実コードでのビットマスクフラグ
ビット演算が小手先のテクニックから実務へ昇格するのが、この節です。
32 ビットに 32 個の真偽値を詰める
32 フィールドの真偽値構造体を作るかわりに、整数 1 つにまとめます。
const FLAGS = {
DARK_MODE: 1 << 0,
NEW_NAV: 1 << 1,
AI_SUGGESTIONS: 1 << 2,
BETA_EDITOR: 1 << 3,
// ... 1 << 31 まで
};
let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS; // 有効化
if (userFlags & FLAGS.AI_SUGGESTIONS) {
showSuggestions();
}
userFlags &= ~FLAGS.DARK_MODE; // 無効化
32 個の真偽値を 4 バイトに収め、任意の部分集合を 1 回の AND で照会できます。データベース側も 32 列ではなく 1 列で済むので、このパターンを好みます。
Unix ファイルパーミッション
chmod 755 は本質的にビット演算です。3 つの 8 進桁が 3 組の 3 ビットに対応します。
7 = 111 (所有者: rwx)
5 = 101 (グループ: r-x)
5 = 101 (その他: r-x)
試してみてください。進数変換ツール で元進数を 8 進にして 755 を入力すると、二進出力は 111101101。ファイルシステムはまさにこの形で権限フィールドを保存しています。
「グループの書き込み」だけ追加するなら、こうです。
const perms = 0o755;
const withGroupWrite = perms | 0o020; // 0o775
IP サブネットマスク
192.168.1.10/24 が与えられたら、マスクと AND を取ってネットワークアドレスを抽出します。
const ip = 0xC0A8010A; // 192.168.1.10
const mask = 0xFFFFFF00; // 255.255.255.0 (/24)
const network = ip & mask; // 0xC0A80100 = 192.168.1.0
パック ID:Snowflake
Twitter の Snowflake は、タイムスタンプ、マシン ID、シーケンスを 64 ビット整数にパックします。
┌─ 1 ビット ─┬─── 41 ビット ───┬─ 10 ビット ─┬─ 12 ビット ─┐
│ 符号 │ タイムスタンプ │ マシン ID │ シーケンス │
└────────────┴─────────────────┴─────────────┴─────────────┘
ID のエンコードは、2 回のシフトと 2 回の OR で済みます。
const id = (BigInt(timestamp) << 22n) |
(BigInt(machineId) << 12n) |
BigInt(sequence);
デコードは逆で、右シフトとマスク。Snowflake、ULID、UUIDv7 の使い分けは分散 ID 比較でまとめています。
言語ごとの落とし穴
JavaScript:32 ビット強制変換の罠
JavaScript はビット演算のたびにオペランドを 32 ビット符号付き整数に変換し、結果を Number に戻します。2³¹ − 1 = 2147483647 を超える値はあふれます。
2147483647 | 0 // 2147483647 (まだ平気)
2147483648 | 0 // -2147483648 (オーバーフロー)
4294967295 | 0 // -1 (全ビット 1 が符号付きに見える)
64 ビットで演算したいなら BigInt を使います。幅制限のない独立したビット演算子が用意されています。
(2n ** 40n) | 1n // 1099511627777n
演算子優先順位のバグ
現場でよく見るビット演算バグの一つです。
// バグ:== が & より優先されるので (x & (1 == 0)) と解釈される
if (x & 1 == 0) { /* ... */ }
// 正しい:括弧を付ける
if ((x & 1) == 0) { /* ... */ }
C、JavaScript、Python、Go など主要な派生言語すべてで、比較演算子の優先度はビット AND/OR/XOR より高くなっています。迷ったら括弧を付ける、で十分です。
言語比較表
| 言語 | 幅の強制 | 負数の >> | BigInt 対応 |
|---|---|---|---|
| JavaScript | 32 ビット符号付きに強制、>>> は符号なし | 算術 | BigInt に独立演算子 |
| Python | 任意精度、固定幅なし | 算術 | 標準装備 |
| Go | 厳格、シフト量は符号なし整数 | 符号付き型は算術 | math/big |
| C/C++ | 型に従う(int、unsigned など) | 符号付きは実装依存 | 標準は無し |
| Rust | 厳格、debug 時はオーバーフローで panic | 符号付きは算術 | u128 や外部 crate |
Python の「無限幅」の罠
Python の整数は固定幅を持たないので、2 の補数論理は概念上無限に左へ拡張されます。そのため ~5 は 250 や 65530 ではなく -6 になります。Python は結果を固定幅のビット列ではなく負の整数として扱うからです。ラップアラウンド挙動が欲しければ、自分でマスクしてください。
# 8 ビット NOT をシミュレート
(~5) & 0xFF # 250
2026 年のパフォーマンス現実
「ビット演算は常に速い」という通説は、2026 年では半分正しく、半分間違いです。
コンパイラは自明な書き換えをすでに行います。現代のオプティマイザは x * 2 を自動で x << 1 に変換します。アプリコードで速度目的に x << 1 を書くのは形だけの最適化で、効果はなく、可読性だけ下がります。
ビット演算が本当に効く場面はこのあたりです。
- 数値処理のホットループ:popcount、先頭 / 末尾ゼロ数、ビットボード将棋エンジン
- コンパクトなデータ構造:Bloom フィルタ、Roaring bitmap、Fenwick 木
- ハードウェアレジスタとメモリマップ I/O:組み込み、カーネル、ファームウェア
- 暗号プリミティブ:AES、ChaCha20、SHA。基本は XOR、ローテーション、シフトの組み合わせ
- 圧縮・解凍:Huffman 符号、ランレングス、パック整数
- データベースエンジン:Bitmap インデックス、Parquet 辞書符号化のようなパック列形式
効かない場面もあります。1 リクエスト 2 回程度呼ばれる業務ロジック関数で x % 2 を x & 1 に書き換える、といったものです。高速化は測定不能で、可読性コストだけが残ります。
ビット操作がいつでも勝てる唯一の場面はメモリフットプリントです。32 フラグを int にパックすれば、32 個の真偽値より 31 バイト減らせます。スケール、たとえば数百万ユーザーや数十億イベントのレベルになると、これがキャッシュ効率の差として効いてきます。
クイックリファレンス
| 操作 | 演算子 | 例 | 結果 | 代表的用途 |
|---|---|---|---|---|
| AND | & | 0b1100 & 0b1010 | 0b1000 | マスク・抽出 |
| OR | | | 0b1100 | 0b1010 | 0b1110 | フラグ合成 |
| XOR | ^ | 0b1100 ^ 0b1010 | 0b0110 | トグル・差分検出 |
| NOT | ~ | ~0b1100 | ...11110011 | 反転マスク |
| 左シフト | << | 1 << 3 | 8 | 2ⁿ で乗算 |
| 右シフト | >> | 16 >> 2 | 4 | 2ⁿ で除算(符号付き) |
| 符号なし右シフト(JS) | >>> | -1 >>> 0 | 4294967295 | 符号なし解釈 |
n ビット目を立てる | | | x | (1 << n) | ビット ON | |
n ビット目を落とす | & ~ | x & ~(1 << n) | ビット OFF | |
n ビット目を反転 | ^ | x ^ (1 << n) | ビット反転 | |
n ビット目を確認 | & | (x >> n) & 1 | 0 か 1 | ビット判定 |
| 最下位の立ちビット | & - | x & -x | 最下位を抽出 | |
| 2 のべき乗判定 | & | x > 0 && (x & (x-1)) == 0 | 真偽 | べき乗検出 |
FAQ
論理 AND(&&)とビット AND(&)の違いは?
論理 AND は真偽値で短絡評価し、条件分岐に使います。ビット AND は整数の各ビットを必ず両辺評価し、マスク抽出やフラグ判定に使い分けます。
なぜ ~1 はたいていの言語で -2 になる?
~1 が -2 になるのは 2 の補数表現のためです。全ビット反転は -(x + 1) と等価なので、~0 は -1、~(-1) は 0 となり主要言語で共通です。
x << 1 は本当に x * 2 より速い?
x << 1 は x * 2 より速くなりません。現代のコンパイラが同じシフト命令へ最適化するため、日常コードは可読性を優先し、ビット意図を示したい場面だけ << を使いましょう。
JavaScript は 64 ビットのビット演算をサポートしていますか?
JavaScript の通常演算子は 32 ビット符号付きに強制変換するため 64 ビットは不可です。BigInt(例:1n << 40n)を使えば任意精度のビット演算が書けます。
立ちビット数を効率的に数えるには?
立ちビット数は言語の組み込みに任せるのが最速です。Go は bits.OnesCount、Java は Integer.bitCount、Python 3.10+ は .bit_count() で、いずれも POPCNT 命令へマップされます。
真偽値構造体ではなくビットマスクフラグを使うべきなのは?
ビットマスクは DB 列やプロトコルヘッダに多数のフラグを詰めたい場合や、flags & REQUIRED_MASK で組み合わせを一括判定したい場合に向きます。可読性重視なら構造体が無難です。
ビット幅を超えてシフトするとどうなる?
ビット幅超えのシフトは C/C++ では未定義動作、JavaScript はシフト量を 32 で剰余し 1 << 32 が 1 になります。Python は無制限なので移植するなら必ず自分でマスクしましょう。
Python で ~5 が 2 ではなく -6 になるのはなぜ?
Python は整数幅が無制限で補数が左へ無限に拡張されるため、~5 は -(5 + 1) = -6 となります。8 ビット反転値 250 が欲しい場合は (~5) & 0xFF のように明示マスクしてください。
XOR 暗号は安全?
XOR 暗号は鍵の使い方次第です。長さ一致の真乱数鍵を 1 回だけ使う OTP は情報論的に安全ですが、鍵の使い回しや短い繰り返し鍵は簡単に破られます。本番は AES / ChaCha20 を選びましょう。
負の数の 2 の補数を手で書くには?
目標ビット幅で正の値を二進表記し、全ビット反転して 1 を加えます。8 ビット -5 なら 00000101 → 11111010 → 11111011。進数変換ツール に 251 を入れて確認できます。
関連ツールと関連記事
- 進数変換ツール 数値を入力してビットパターンをその場で確認
- 進数変換完全ガイド 二進・八進・十六進の前提知識
- UUID v4・v7・ULID・Snowflake 比較 分散 ID におけるビットパック
- セキュリティベストプラクティス 権限ビットマップとその落とし穴