本記事では、Rubyの整数オブジェクト(Integerクラス)におけるビット単位演算子 (~、&、|等) の挙動と、その背後にある仕組みを解説します。
この記事を読めば、~10 のようなビット単位NOT演算が、なぜ -11 という直感的でない結果になるのかを明確に理解できるようになります。
目次
- Rubyの整数の特徴
- 多倍長整数
- 2の補数
- Rubyにおけるビット単位NOT (
~) の仕組み - Rubyにおけるビット単位AND (&) の仕組み
- Rubyにおけるビット単位OR (|) の仕組み
- Rubyにおけるビット単位XOR (^)の仕組み
- まとめ
Rubyの整数の特徴
Rubyでビット演算を理解するには、まずRubyの整数が持つ2つの重要な特徴を知る必要があります。
- 多倍長整数であること
- 2の補数表現であること
この2つの組み合わせが、ビット演算、特にNOT演算の挙動を理解する鍵となります。
多倍長整数
Rubyの整数には、一般的なプログラミング言語にあるような「32ビット整数」や「64ビット整数」といったサイズの上限がありません。利用可能なメモリが許す限り、どこまでも大きな数値を扱うことができます。
このような整数を多倍長整数(Arbitrary-precision integer)と呼びます。
コンピュータのメモリは、数値を2進数で格納します。多倍長整数を2進数でイメージする場合、桁数が事実上無限であると考えるのが分かりやすいでしょう。
例えば、10という整数を2進数で表現すると、通常は 1010 となりますが、Rubyの内部では、その先頭に無限の0が続いている(...000000001010)とイメージしてください。
10.to_s(2)
# => "1010"
# イメージ: ...000000001010
2の補数表現
「2の補数」とは、限られたビット数の中で負の数を効率的に表現するために考案された形式です。Rubyは多倍長整数でありながら、この2の補数表現を採用しています。
まずは、基本となる固定長のビット数(例:3ビット)で2の補数を理解しましょう。
3ビットでは、23=8通りの値しか表現できません。これを符号なし整数として使うと0から7までを表現できますが、2の補数を使うと、表現できる正の数の範囲が狭まる代わりに、負の数を表現できるようになります。
| 2進数 (3ビット) | 符号なし10進数 | 2の補数 (符号付き10進数) |
| 0 | 0 | 0 |
| 1 | 1 | 1 |
| 10 | 2 | 2 |
| 11 | 3 | 3 |
| 100 | 4 | -4 |
| 101 | 5 | -3 |
| 110 | 6 | -2 |
| 111 | 7 | -1 |
2の補数の定義
そもそも「2の補数」という名前は、その数学的な定義に由来します。 nビットの世界において、ある数 x とその符号を反転させた数(ここでは -x と表現)を足すと、2n(2のn乗)になる、という関係性を指します。
x + (-x) = 2^n
例えば、3ビットの場合で見てみましょう。
2は010-2は110
この2つを2進数で足すと 010 + 110 = 1000 となります。1000 は10進数で 8、つまり2^3であり、定義と一致します。
コンピュータのnビット計算では、nビットを超える桁(この例では4ビット目の1)は表現できずにあふれてしまいます(オーバーフロー)。この結果、あふれた桁は無視され 000 となるため、コンピュータ上では x + (-x) = 0 という計算が正しく成立します。
これが、2の補数の定義となります。
この定義から、ビット演算を理解する上で特に重要な以下の3つの特徴が導かれます。
1. 最上位ビットは符号を表す(符号ビット)
一番左のビットが 0 なら正の数、1 なら負の数となります。
2. ビット数を増やしても値は変わらない(符号拡張)
2の補数表現では、ビット数を増やす際に、元の最上位ビット(符号ビット)を新しい桁にコピーして埋めます。これを符号拡張と呼びます。
| 3ビット | 4ビットに拡張 | 10進数 |
| 10 | 10 | 2 |
| 110 | 1110 | -2 |
この性質を多倍長整数に当てはめると、以下のようになります。
- 正の数: 先頭には無限の
0が続いている。(例:2は...00000010) - 負の数: 先頭には無限の
1が続いている。(例:-2は...11111110)
3. ある数 x の正負を反転した数は「ビット反転 + 1」で得られる
先ほどの定義 x + (-x) = 2^n を変形すると、-x = 2^n - x となります。この式をさらに変形すると -x = (2^n - 1 - x) + 1 となり、これは「x の全ビットを反転したものに1を加える」という操作と同じ意味になります。
これはなぜかというと、例えばn=3ビットだと、2^3 = 1000になります。この数は4ビットなので実際には3ビットで表現できませんが、1000 – 1をすると111になるので3ビットで表現できるようになります。つまり、2^n-1というのはnビット上で全てのビットが1の数ということです。
一般的に、何ビットの数であれ、各ビットが全て1の数から別の数xを引くのは、xのビット反転と同義です。これは各ビットにおいて、1 – 1の場合は0、1 – 0の場合は1となるためです。この性質は、無限ビットである多倍長整数でも変らず、2^n-1は各ビットが1の無限の数となり、そこからxを引くということはxのビット反転と同義となります。
例えば、2 (...0000010) の符号を反転させて -2 を作るには、
- 全ビットを反転:
...0000010→...1111101 - 1を加える:
...1111101+1→...1111110(これは-2を表します)
この「-x = (xのビット反転) + 1」という関係は非常に重要なので覚えておきましょう。
Rubyにおけるビット単位NOT (~) の仕組み
いよいよ本題です。ビット単位NOT (~) は、その名の通り、対象となる数値の全ビットを反転させる演算子です。
~10 を例に考えてみましょう。
10 の2進数表現は ...00001010 です。
ビット単位NOTは、この無限に続くビットをすべて反転させます。
...000000001010 → ...111111110101
反転後のビット列は、最上位ビットが 1 なので負の数であることが分かります。では、この ...11110101 は10進数でいくつになるのでしょうか?
ここで、先ほどの2の補数の性質を使います。
~xはxのビット反転ですので、先ほどの公式「-x = (xのビット反転) + 1」は「-x = ~x + 1」となり、これを変形すると、以下のようになります。
~x = -x - 1
この公式に x=10 を代入してみましょう。
~10 = -10 - 1 = -11
このように、Rubyの整数が多倍長整数であり、2の補数表現を採用していることから、ビット単位NOT (~) 演算は「元の数にマイナスを付けて1を引く」という計算と同じ結果になるのです。
Rubyにおけるビット単位AND (&) の仕組み
ビット単位ANDは、2つのビットパターンの同じ位置のビット同士を比較し、両方とも1であれば1を、そうでなければ0を返す演算です。
Rubyの整数は、多倍長整数と2の補数表現によって、以下の特徴を持ちます。
正の整数:符号ビットを含む先頭行は無限に続く0で構成される。
負の整数:符号ビットを含む先頭行は無限に続く1で構成される。
この特徴により、Rubyの整数同士のビット単位ANDは、無限に続く先頭行の各ビット同士のANDで戻り値の符号が決まり、以下の表のような結果になります。
| 演算 | 符号ビット含む先頭ビット列のAND | 結果の符号 |
| 正 & 正 | …0 & …0=…0 | 常に正(または 0) |
| 負 & 負 | …1 & …1=…1 | 常に負 |
| 正 & 負 | …0 & …1=…0 | 常に正(または 0) |
また、入力値と結果の値は、以下の大小関係を持ちます。
| 演算 | 結果 | 例 |
| 正 & 正 | 0以上、小さい方の正の整数以下 | 3 & 5なら0以上3以下 |
| 負 & 負 | 小さい(絶対値が大きい)方の負の整数以下、合計額以上 | -2 & -20なら-20以下、-22以上 |
| 正 & 負 | 0以上、正の整数以下 | 2 & -20なら0以上2以下 |
これは、以下のような論理で成り立ちます。
正の整数同士のビット単位AND
- 2つの入力値をA、B、結果をCとします。
- Cは前述の通り符号ビットが0なので、0以上です。
- Cは必ずA以下になります。これは、正の整数はより高い位に1がある方がより大きい数であり、それより下位のビットも1がある方が大きい数字ですが、ビットAND演算で1が0になることはあっても、0が1になることはないからです。つまり、値が小さくなることはあっても、大きくなることはありません。
- 同様にCは必ずB以下になります。
- 結果、Cは0以上、かつAとBの小さい方の整数以下になります。0 <= C <= min(A, B)
負の整数同士のビット単位AND
2つの入力値をA、B、結果をCとします(A < 0, B < 0, C = A & B)。
結果の範囲
上限:
- C は必ず A 以下になります。AND演算の性質上、A のビットが 1 で B のビットが 0 だった場合、C の対応するビットは 0 になります。負の数において、最上位から見て最初に食い違うビットが 1 から 0 に変わると、その数はより小さくなります。C は Aに比べてビットが1から0に変わることはあっても、0から 1 に変わることはないため、CがAより大きくなることはありません。
- 同様にCは必ずB以下になります。
- 結果、CはAとBの小さい方の整数以下になります。C <= min(A, B)
下限:
Cは常に A と B の合計以上になります。これは、ビット演算における以下の恒等式によって説明できます。
A + B = (A | B) + (A & B) (ここで | はビット単位ORです)
この式を A & B について変形します。
A & B = (A + B) - (A | B)
ここで、A と B が両方とも負の整数である場合を考えます。
AとBが負:AもBも、上位ビットは無限に続く...111です。A | B(OR演算):...111 | ...111となるため、結果(A | B)も必ず負の整数(...111)になります。A | Bは負:つまり、(A | B) <= -1が成り立ちます。
A & B = (A + B) - (A | B) の式に戻ると、(A | B) はマイナスの値です。 「マイナスの値を引く」ということは、「プラスの値を足す」ことと同じです。
したがって、C(A & B) の値は、(A + B) よりも大きい値に必ずなります。
C >= A + B
したがって、負の整数同士のANDには以下の関係が成り立ちます。
(A + B) <= C <= min(A, B)
正の整数と負の整数のビット単位AND
- 2つの入力値の正の整数をA、負の整数をB、結果をCとします。
- Cは前述の通り符号ビットが0なので、0以上です。
- Cは必ずA以下になります。これは、正の整数はより高い位に1がある方がより大きい数であり、それより下位のビットも1がある方が大きい数字ですが、BとのビットAND演算でAの1が0になることはあっても、Aの0が1になることはないからです。つまり、Aの値が小さくなることはあっても、大きくなることはありません。
- 結果、Cは0以上、かつA以下となります。0 <= C <= A
ビット単位ANDのまとめ
まとめると、以下の表のようになります。
| A | B | Cの符号 | Cの範囲 |
| 正の整数 | 正の整数 | 正 | 0 <= C <= min(A, B) |
| 負の整数 | 負の整数 | 負 | (A + B) <= C <= min(A, B) |
| 正の整数 | 負の整数 | 正 | 0 <= C <= A |
このように、Rubyにおいて整数同士のビット単位ANDの結果は、入力値の符号と絶対値から上限と下限が決定されます。このことを覚えておくと、入力値に対して予想外の結果が返った時にバグの可能性を疑えるようになります。
Rubyにおけるビット単位OR (|) の仕組み
ビット単位ORは、2つのビットパターンの同じ位置のビット同士を比較し、両方とも0であれば0を、そうでなければ1を返す演算です。
ORの組み合わせ表
| 0 | 1 | |
| 0 | 0 | 1 |
| 1 | 1 | 1 |
Rubyにおける整数同士のビット単位ORの結果は、入力値の符号によって変わります。
正の整数同士のビット単位OR
2つのの正の整数をA、Bとし、AとBのビット単位ORの結果をCとします(A >=0, B >=0, A > B, C = A | B)。
結果の符号
無限符号ビットはAとB両方とも0です。0|0 の結果は0なので、Cの無限符号ビットも0となり、Cは必ず正の整数になります。
| 無限符号ビット | |
| A | …0 |
| B | …0 |
| C | …0 |
結果の範囲
下限:無限符号ビットより下位のAのビットにおいて、あるビット値が0の時、Cの同位ビットが1になる可能性はあります(Bの同位ビットが1の場合)。逆に、Aのビット値が1の時は、Bの同位ビットがなんであれ、Cのビット値は1になります。つまり、CがA以上になる可能性はありますが、A未満になる可能性はなく、C >= Aが成り立ちます。同様に、C >= Bも成り立ちます。つまり、max(A, B)が下限です。
上限:A | B <= A + B という関係が常に成り立ちます。これは A + B = (A | B) + (A & B) という恒等式で説明できます(正の整数同士のA & B はビット単位ANDで常に0以上)。
したがって、結果Cの範囲は以下のようになります。
A <= C <= A + B
負の整数同士のビット単位OR
2つの負の整数をA、Bとし、、AとBのビット単位ORの結果をCとします。(A < 0, B < 0, C = A | B)
結果の符号
2の補数表現で、負の数の無限符号ビットは1です。1 | 1の結果は1なので、Cの無限符号ビットも1になり、Cは必ず負の整数になります。
| 無限符号ビット | |
| A | …1 |
| B | …1 |
| C | …1 |
結果の範囲
下限:2の補数表現での負の数は、符号ビット以外のビットで1が多ければ多いほど、値は増加します(-1に近づきます)。Aのあるビットにおいて値が0の時にBとのOR演算によりCの同位ビットが1になることはあります。逆に、Aのビットが1の時は、Cの同位ビットが0になることはなく、必ず1になります。つまり、C >= Aが成り立ちます。同様に、B >= Bも成り立ちます。つまり、max(A, B) <= Cとなります。
上限:負の整数は全てのビットが1になった時、すなわち-1が上限です。ビット単位ORでは入力値の組み合わせによってビットが全て1になる可能性があります。つまり、-1がCの上限となります。
まとめると、負の整数同士のビット単位ORの取りうる範囲は以下の通りです。
max(A, B) <= C <= -1
例:
# -9と-3であれば、以下の範囲になる。
# max (-9, -3) <= c <= -1
# -3 <= c <= -1
-9 | -3
=> -1
正の整数と負の整数のビット単位OR
正の整数をA、負の整数をB、AとBのビット単位ORの結果をCとします。(A >= 0, B < 0, C = A | B)
結果の符号
無限符号ビットはAが0、Bが1なので、0 | 1 の結果は1になります。つまり、Cは必ず負の整数になります。
| 無限符号ビット | |
| A | …0 |
| B | …1 |
| C | …1 |
結果の範囲
下限:2の補数表現での負の数は、符号ビット以外のビットで1が多ければ多いほど、値は増加します(-1に近づきます)。Bのあるビットにおいて値が0の時にAとのOR演算によりCの同位ビットが1になることはあります。逆に、Bのビットが1の時は、Cの同位ビットが0になることはなく、必ず1になります。つまり、C >= Bが成り立ちます。
上限:負の整数の最大値は全てのビットが1の時、すなわち-1です。ビット単位ORでは入力値の大小に関わらず、組み合わせによって全てのビットが1になる可能性があるため、-1がそのままCの上限値となります。
したがって、結果Cの範囲は以下のようになります。
B <= C <= -1
例: 4 | -9
4: ...00000100
-9: ...11110111
C: ...11110111 = -9
4 | -9
=> -9
範囲の確認: -9 <= -9 <= -1。範囲内に収まっています。
ビット単位ORのまとめ
全ての組み合わせをまとめると、以下の表の通りです。
| A | B | Cの符号 | Cの範囲 |
| 正の整数 | 正の整数 | 正 | max(A, B) <= C <= A + B |
| 負の整数 | 負の整数 | 負 | max(A, B) <= C <= -1 |
| 正の整数 | 負の整数 | 負 | B <= C <= -1 |
このように、Rubyにおいて整数同士のビット単位ORの結果は、入力値の符号と絶対値から上限と下限が決定されます。このことを覚えておくと、入力値に対して予想外の結果が返った時にバグの可能性を疑えるようになります。
Rubyにおけるビット単位XOR (^)の仕組み
ビット単位XOR(ビットごとの排他的論理和とも呼ぶ)は、2つのビットパターンの同じ位置のビット同士を比較し、両者が違う値であれば1を、そうでなければ0を返す演算です。
XORの組み合わせ表
| 0 | 1 | |
| 0 | 0 | 1 |
| 1 | 1 | 0 |
Rubyにおける整数同士のビット単位XORの結果は、入力値の符号によって変わります。
正の整数同士のXOR
2つのの正の整数をA、Bとし、AとBのビット単位XORの結果をCとします(A >=0, B >=0, A > B, C = A ^ B)。
結果の符号
無限符号ビットはAとB両方とも0です。0 ^ 0 の結果は0なので、Cの無限符号ビットも0となり、Cは必ず正の整数になります。
| 無限符号ビット | |
| A | …0 |
| B | …0 |
| C | …0 |
結果の範囲
下限:0 です。これはAとBが同じ値の場合(A == B)、すなわち各ビット値が同一のため、各ビットが0になる場合に発生します (例: 5 ^ 5 = 0)。
上限:上限: A + B です。XORは「繰り上がりのない2進数の足し算」です。A と B に共通のビット(両方とも1 の位置)が全くない場合、XORの結果は単純な足し算 A + B と一致します (例: 4 (100) ^ 2 (010) = 6 (110))。共通ビットがあると、その桁が 0 になるため、A + B より小さい値になります。
したがって、両方とも正の整数の場合のXORの結果範囲は以下の通りです。
0 <= C <= (A + B)
負の整数同士のXOR
AとBが共に負の整数で、CをXORの結果とします(A < 0, B < 0, C = A ^ B )。
結果の符号
無限に続く 1 同士のXORは 0 になります (1 ^ 1 = 0)。したがって、結果は必ず ...000... となり、正の整数になります。
結果の範囲
下限: 0 です。これは a と b が同じ値の場合(a==b)に発生します (例: -5 ^ -5 = 0)。
上限:∣A∣+∣B∣−2 です(|A|と|B|はそれぞれAとBの絶対値)。
これは A ^ B が (|A| - 1) ^ (|B| - 1) と等しくなるという性質に基づいています。
(例: A = -3, B = -5 の場合。∣A∣=3,∣B∣=5。
(-3) ^ (-5) の結果は (|3| – 1) ^ (|5| – 1) = 2 ^ 4 = 6 となります。
このケースの上限は 3+5−2=6 となり、上限値に達しています。)
したがって、両方が負の整数の場合のXORの結果は以下の通りです。
0 <= C <= ∣A∣+∣B∣−2
正の整数と負の整数のXOR
Aを正の整数、Bを負の整数、AとBのXORの結果をCとします(A >= 0, B <0, C = A ^ B)。
結果の符号
無限に続く 0 (正の数) と 1 (負の数) のXORは 1 になります (0 ^ 1 = 1)。したがって、結果は必ず ...111... となり、負の整数になります。
結果の範囲
下限(最も小さい値): −(A+∣B∣) です。
例: A = 4 (…0100), B = -3 (…101) の場合。
∣B∣=3。
4 ^ (-3) の結果は -7 です。
このケースの下限は −(4+3)=−7 となり、下限値に達しています。
上限(最も大きい値): -1 です。 負の整数の上限は-1(…1)であり、全てのビットが1の数です。Bのビットと全てのビットが異なるAをXORすれば各ビットが全て1になります。
例:A = 4 (…0100), B = -5 (…1011)
…0100 ^ …1011 = …1111 = -1
したがって、正の整数と負の整数のXORの範囲は以下の通りです。
−(A+∣B∣) <= C <= -1
ビット単位XORのまとめ
まとめると、以下の表の通りです。
| A | B | Cの符号 | Cの範囲 |
| 正の整数 | 正の整数 | 正 | 0 <= C <= (A + B) |
| 負の整数 | 負の整数 | 正 | 0 <= C <= ∣A∣+∣B∣−2 |
| 正の整数 | 負の整数 | 負 | −(A+∣B∣) <= C <= -1 |
まとめ
- Rubyの整数は、桁数に制限のない多倍長整数であり、無限に続くビット列とイメージできる。
- 負の数の表現には2の補数が使われている。
- この2つを理解すると、Rubyの整数に対するビット単位演算の挙動も理解できる。
一見すると不思議なビット単位演算の挙動も、背景にあるコンピュータの数値表現を理解することで、その理由が明確になります。
