ビット演算
<< 左シフト ── 桁を左へずらす。1桁ずらすと2倍。ただし上位はこぼれ落ちる
ずらす桁数0〜31・かならず10進で読みますそのまま貼れる式2進で並べる| どれ | 2進(32桁・4桁ずつ) | 10進 |
|---|---|---|
| 1つめ0x00000001 | 0000 0000 0000 0000 0000 0000 0000 0001 | 1 |
| << ずらす桁数 | 10進で表します | 31 |
| = 結果0x80000000 | 1000 0000 0000 0000 0000 0000 0000 0000 | -2147483648符号なしなら 2147483648 |
表は横にスクロールできます。32桁ぶんの2進が右へ続いています。
結果のビットを、2とおりに読むと| 符号つきで読むとsigned 32bit | -2147483648← これが返ってきます |
|---|---|
| 符号なしで読むとunsigned 32bit | 2147483648 |
答えの32桁目(いちばん上のビット)が1になったので、符号つきで読むと負になりました。捨てられたビットはありません。同じビットを符号なしで読めば 2147483648 です。
同じ式を64bitで計算すると 2147483648。答えが変わります。
やってみる
1問目 / 全6問
よくある質問
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つい思う答え 1 | 1 ── 0101 と 0011 で、両方1なのはいちばん下の桁だけ。 |
| 5 | 3つい思う答え 7 | 7 ── 0101 と 0011 で、どちらかが1なのは下3桁。0111 = 7。 |
| 5 ^ 3つい思う答え 6 | 6 ── 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 として出てきます。
−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、または 0 | 1 ── 仕様が「ずらす桁数は32で割った余り」と決めている。32の余りは0なので、ずらさないのと同じ。 |
| 1 << 33つい思う答え 8589934592 | 2 ── 33の余りは1。1桁だけずれる。33桁ずらしたつもりが2になる。 |
| 1 << -1つい思う答え 0、またはエラー | -2147483648 ── −1 は符号なし32bitとして 4294967295 と読まれ、その32の余りは31。つまり 1 << 31 と同じ。 |
| -1 >>> 0つい思う答え -1 | 4294967295 ── 1桁もずらしていないのに値が変わる。>>> だけが答えを符号なしで返すから。 |
| -1 >> 0つい思う答え -1 | -1 ── こちらは符号つきで返るので −1 のまま。>> と >>> の違いはここだけ。 |
| 4294967296 | 0つい思う答え 4294967296 | 0 ── 2の32乗は32桁の外。「| 0」は何も変えないつもりの書き方だが、32bit に畳む処理だけが効いて0になる。 |
| 4294967297 | 0つい思う答え 4294967297 | 1 ── 上の値に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 の演算子の説明(新しいタブで開きます)です。単体テストが、画面に出す値と仕様どおりの数字を突き合わせています。