調べて分かる道具箱

ビット演算

1つめの数
演算

<< 左シフト ── 桁を左へずらす。1桁ずらすと2倍。ただし上位はこぼれ落ちる

ずらす桁数0〜31・かならず10進で読みます
ビット幅
例を入れる
そのまま貼れる式1 << 312進で並べる
入れた数と結果を、32桁の2進で上下にそろえたもの
どれ2進(32桁・4桁ずつ)10進
1つめ0x000000010000 0000 0000 0000 0000 0000 0000 00011
<< ずらす桁数10進で表します31
= 結果0x800000001000 0000 0000 0000 0000 0000 0000 0000-2147483648符号なしなら 2147483648

表は横にスクロールできます。32桁ぶんの2進が右へ続いています。

結果のビットを、2とおりに読むと
結果を符号つき・符号なしで読んだ値
符号つきで読むとsigned 32bit-2147483648← これが返ってきます
符号なしで読むとunsigned 32bit2147483648

答えの32桁目(いちばん上のビット)が1になったので、符号つきで読むと負になりました。捨てられたビットはありません。同じビットを符号なしで読めば 2147483648 です。

同じ式を64bitで計算すると 2147483648。答えが変わります。

やってみる

1問目 / 全6問

JavaScript で 5 & 3 はいくつになりますか?

よくある質問

JavaScript のビット演算が32bitになるのはなぜですか?

JavaScript の数は本来64bitの浮動小数点数ですが、ビット演算だけは仕様が「まず32bitの整数に直してから計算する」と定めているためです。ECMAScript の仕様書では、数値どうしのビット演算の前に ToInt32、符号なし右シフトの前に ToUint32 を通すと書かれています。そのため32bitを超える上位のビットは、何の警告もなく捨てられます。4294967296 に何もしないはずの「| 0」を書くだけで 0 になるのはこのためです。

1 << 32 が 0 ではなく 1 になるのはなぜですか?

ずらす桁数が32で割った余りに読み替えられるからです。ECMAScript の左シフトの定義に「shiftCount は rnum を32で割った余りとする」と書かれています。32を32で割った余りは0なので、1桁もずらさないのと同じ結果、つまり 1 のままになります。33 なら余りは1なので 2、64 でも余りは0なので 1 です。マイナスも同じで、1 << -1 は 1 << 31 と同じ −2147483648 になります。

-1 >>> 0 が 4294967295 になるのはなぜですか?

1桁もずらしていないのに値が変わります。−1 のビットは32桁すべてが1で、これを符号つきで読むと −1、符号なしで読むと 4294967295 です。ビット演算の中で >>> だけが答えを符号なしで返すため、同じビットの並びが別の数として出てきます。同じ式を >> に変えると −1 のままです。ビットは何も変わっておらず、読み方だけが違います。

32bitより大きなビット演算をしたいときはどうしますか?

BigInt を使います。BigInt どうしの & | ^ ~ << >> は32bitに畳まれず、桁数の上限もありません。ただし BigInt には決まった桁数が無いので、64bitとして扱いたいときは BigInt.asIntN(64, x) や BigInt.asUintN(64, x) で自分から桁数を決める必要があります。たとえば 1n << 63n は 9223372036854775808 という正の数のままで、64bitの符号つきとして読みたければ asIntN を通して −9223372036854775808 にします。

BigInt で >>> が使えないのはなぜですか?

9n >>> 2n と書くと TypeError になります。ECMAScript の仕様が、BigInt の符号なし右シフトを「TypeError を投げる」とだけ定めているためです。符号なし右シフトは「空いた上位の桁に0を詰める」操作ですが、BigInt には決まった桁数が無いので、どこまでを上位と呼ぶのかが決まりません。64bitとして扱うなら BigInt.asUintN(64, x) >> 2n のように、桁数を自分で書く必要があります。

~5 が −6 になるのはなぜですか?

全部の桁を0と1で入れ替えるからです。2の補数では、ある数の全ビットを反転して1を足すとその数の負の値になります。つまり反転しただけの ~x は、−x から1を引いた −x−1 になります。5 なら −6、0 なら −1、−1 なら 0 です。この規則は入れた数によらず必ず成り立ちます。

AND・OR・XOR・NOT とシフトを、2進の桁をそろえて並べながら計算します。 入れた数と結果が縦に並ぶので、どの桁とどの桁を見比べているのかが目で追えます。

ビット演算は、桁ごとに見比べているだけです

ビット演算は、数を2進で書き直してから、同じ位置の桁どうしを見比べる計算です。5 は 0101、3 は 0011。この2つを並べて、桁ごとに「両方1か」を見れば AND、 「どちらかが1か」を見れば OR、「違うか」を見れば XOR になります。 だから上の道具は、答えだけでなく2進を桁までそろえて並べます。

まずはそのまま動く例
式返る値と、その理由
5 & 3つい思う答え 11 ── 0101 と 0011 で、両方1なのはいちばん下の桁だけ。
5 | 3つい思う答え 77 ── 0101 と 0011 で、どちらかが1なのは下3桁。0111 = 7。
5 ^ 3つい思う答え 66 ── 0101 と 0011 で、違うのは下から2桁目と3桁目。0110 = 6。
~5つい思う答え -6-6 ── 全桁を入れ替える。2の補数では ~x が必ず −x−1 になる。

2進や16進そのものを読み書きしたいときは、n進数の変換のページが担当しています。あちらは同じ数を別の書き方にするところまで、 こちらはその数どうしを演算するところを引き受けています。

ここから先が本題です。答えはいつも32bitに畳まれます

JavaScript の数は、本来64bitの浮動小数点数です。 ところがビット演算だけは別あつかいで、いったん32bitの整数に直してから計算すると仕様に書かれています。 数値どうしの演算の前に ToInt32、>>> の前に ToUint32 を通す、という書き方です。

つまり32bitに入りきらない上位のビットは、何も言われずに捨てられます。4294967296 | 0 は、何も変えないはずの書き方なのに 0 になります。 2の32乗は32桁の窓の外なので、下32桁に残っているのが全部0だからです。 1を足した 4294967297 なら 1 が返ります。

上の道具は、入れた数が32bitに収まらないときに、そう画面に出します。 黙って畳んだ値だけを見せると、「入力した数と違う数で計算された」ことに気づけないからです。 JavaScript がそうすることは止められませんが、言うことはできます。

1 << 31 が負になるのは、32桁目が符号の桁だからです

1 << 31 は 2147483648 ではなく−2147483648 になります。1を31桁ずらすと、1 がいちばん上(32桁目)に立ちます。 32bitの符号つきでは、この桁が符号を表す約束になっているので、立った瞬間に負として読まれます。

大事なのは、ビットは1つも捨てられていないことです。 並びは同じで、読み方だけが変わっています。 だから(1 << 31) >>> 0 と書けば、同じビットが 2147483648 として出てきます。

同じビットを2とおりに読む符号なしで読むと(>>> が返す答え)0 〜 2147483647いちばん上のビットが 02147483648 〜 4294967295いちばん上のビットが 1変わらないビットの並びは同じ読み方だけが違う0 〜 2147483647いちばん上のビットが 0−2147483648 〜 −1いちばん上のビットが 1符号つきで読むと(& | ^ ~ << >> が返す答え)0xFFFFFFFF は、符号なしなら 4294967295・符号つきなら −1
同じ32桁のビットでも、符号つきで読むか符号なしで読むかで数が変わります。 前半(いちばん上のビットが0)は同じで、後半だけが食い違います。 枠を太くしてあるのがその後半ですが、 どちらの読み方かは箱の中の文字にも書いてあります。 狭い画面では、図を横にスクロールできます。

−1 >>> 0 が 4294967295 になります

ここがこのページでいちばん驚くところです。-1 >>> 0 は、0桁ずらすと書いてあります。何もしていません。 それなのに答えは4294967295 になります。

−1 のビットは32桁ぜんぶが1です。この並びを符号つきで読めば −1、 符号なしで読めば 4294967295。数はどちらも正しく、ビットは1つも動いていません。 ビット演算のうち>>> だけが答えを符号なしで返すので、演算子をひとつ変えただけで読み方が入れ替わります。

確かめ方は簡単で、同じ式の >>> を >> に変えると−1 のままです。上の道具で演算だけを切り替えると、 2進の並びが1桁も変わらないまま、10進の欄だけが入れ替わるのが見えます。

1 << 32 は 0 ではなく 1。桁数が32で割った余りになるからです

32桁の数を32桁ずらせば、全部こぼれ落ちて 0 になりそうです。 ところが答えは1です。 仕様の左シフトの定義に「ずらす桁数は32で割った余りとする」と書かれていて、 32を32で割った余りは0 ── つまり1桁もずらさないことになるからです。

だから 33 なら余り1で 2、64 でも余り0で 1 です。 マイナスも同じ道を通ります。−1 は符号なし32bitとして 4294967295 と読まれ、 その32の余りは31。1 << -1 は1 << 31 と同じ −2147483648 になります。

上の道具は、32以上のシフトを引き受けません。答えを出すことはできますが、それは「ずらしたつもりの桁数」とは違う答えで、 しかも見た目には正しい顔をしています。 黙って剰余した数を返すより、なぜ出せないのかを書くほうが役に立つと考えました。

ここで転ぶ(値はすべて実際に評価したもの)
式返る値と、その理由
1 << 31つい思う答え 2147483648-2147483648 ── 32桁の左端は符号の桁。そこに1が立った瞬間、符号つきでは負として読まれる。ビットは捨てられていない。
1 << 32つい思う答え 4294967296、または 01 ── 仕様が「ずらす桁数は32で割った余り」と決めている。32の余りは0なので、ずらさないのと同じ。
1 << 33つい思う答え 85899345922 ── 33の余りは1。1桁だけずれる。33桁ずらしたつもりが2になる。
1 << -1つい思う答え 0、またはエラー-2147483648 ── −1 は符号なし32bitとして 4294967295 と読まれ、その32の余りは31。つまり 1 << 31 と同じ。
-1 >>> 0つい思う答え -14294967295 ── 1桁もずらしていないのに値が変わる。>>> だけが答えを符号なしで返すから。
-1 >> 0つい思う答え -1-1 ── こちらは符号つきで返るので −1 のまま。>> と >>> の違いはここだけ。
4294967296 | 0つい思う答え 42949672960 ── 2の32乗は32桁の外。「| 0」は何も変えないつもりの書き方だが、32bit に畳む処理だけが効いて0になる。
4294967297 | 0つい思う答え 42949672971 ── 上の値に1を足しただけ。下32桁の 1 だけが生き残る。

64bitを出すなら BigInt。ただし >>> は受け付けません

32bitの窓から出たいときはBigIntを使います。 末尾に n を付けた数どうしなら、& | ^ ~ << >> は32bitに畳まれず、桁数の上限もありません。

上限が無いことが、そのまま次の落とし穴になります。1n << 63n は 9223372036854775808 という正の数のままで、64bitの符号つきにはなりません。 BigInt には「何桁の数か」という情報がそもそも無いからです。 64桁として読みたければBigInt.asIntN(64, x) を通して、桁数を自分で決める必要があります。上の道具の 64bit はこれを通しているので、同じ式でも 32bit と答えが変わります。

そして>>> は BigInt を受け付けません。9n >>> 2n と書くと、値が返る代わりにTypeError(BigInts have no unsigned right shift, use >> instead)になります。 仕様が 「TypeError を投げる」とだけ定めているためで、空いた上位の桁に0を詰めようにも、どこからが上位なのかが決まらないからです。 自分で組むならBigInt.asUintN(64, 9n) >> 2nのように、桁数を書いてから普通の右シフトにします。 上の道具の 64bit の符号なし右シフトも、中でこれを組み立てています。

値の出どころ: 画面に出しているビット演算の結果は、すべてその場で実際に評価したもので、手で書き写した数字はありません。 定義の出どころはECMAScript 言語仕様(新しいタブで開きます)(左シフトの「32で割った余り」、BigInt の符号なし右シフトが TypeError を投げること)と、MDN の演算子の説明(新しいタブで開きます)です。単体テストが、画面に出す値と仕様どおりの数字を突き合わせています。