2.9 ビット演算子、シフト演算子

(1/1)
緑色の2進数が流れる背景を背にして立つ陽奈
0と1で表すデジタルの世界
AI生成コンテンツ / AI-generated content
JavaScriptには、ビット演算子として、ビット論理和やビット論理積に加え、シフト演算子が備わっている。フラグ値を1つの変数に収めたり、RGBのように複数のデータを1つにまとめた値を分解・結合するときにビット演算子が活躍する。
また、IoT(モノのインターネット)でデバイスの状態や計測値の入力を行う際、ビット演算を行うことが多い。
(2024年2月10日)「コラム:機械語、コンパイラ、インタプリタ」追記。TeX画像をMathJaxに置き換え。

目次

サンプル・プログラム

ビット論理和とビット論理積

Microsoft Word
Microsoft Word
Microsoft Wordでは、1文字ずつ、イタリック体、ボールド文字、下線を指定することができる。これをプログラムに実装することを考える。
ここでは話を簡単にするため、下表のパラメータのみを指定できるものとする。
文字装飾
文字装飾取り得る値
ボールドON / OFF
イタリックON / OFF
下線ON / OFF
文字ごとに3つの真偽値を用意する方法もあるが、ここでは3種類のON/OFFを、1つの整数にまとめる方法を考える。ON/OFFを表す目印をフラグと呼ぶ。

上表をもう一度見てみよう。
文字装飾の情報量は、それほど多くない。ボールド、イタリック、下線は2値だから、各々の情報量は1ビット。3つでも、わずか3ビットである。この3ビットを1つの変数にまとめてみよう。0bで始まる数値は、JavaScriptの2進数表記である。ここでは右端を「第1ビット」と呼ぶが、ビット番号を0から数える資料もある。
文字装飾
 下線
第3ビット
0b100
イタリック
第2ビット
0b010
ボールド
第1ビット
0b001
ON111
OFF000
ビット演算子
ビット演算子
変数 attribute の値が2進数の 001 なら「第1ビットが立っている=第1ビットがON=ボールド体」と解釈する。同様に、010 ならイタリック体、100 なら下線となる。
組み合わせも可能で、011 ならボールドかつイタリック体、111 ならボールドかつイタリック体かつ下線というように解釈する。
変数 attribute にビット値を設定したり取り出すのに使うのがビット演算子だ。

bit1.html

  20: /**
  21:  * 計算と画面表示
  22: */
  23: function execute() {
  24:     //定数:文字装飾データのビット位置
  25:     const bold      = 0b001;
  26:     const italic    = 0b010;
  27:     const underline = 0b100;
  28: 
  29:     //文字装飾データを格納する変数
  30:     let attribute   = 0b000;
  31: 
  32:     //form要素を取得
  33:     let element = document.getElementById('myform');
  34: 
  35:     //ラジオボタングループ name='bold' の値を取得
  36:     if (element.bold.value == 'on') {
  37:         attribute |= bold;
  38:     } else {
  39:         attribute &= ~bold;
  40:     }
  41: 
  42:     //ラジオボタングループ name='italic' の値を取得
  43:     if (element.italic.value == 'on') {
  44:         attribute |= italic;
  45:     } else {
  46:         attribute &= ~italic;
  47:     }
  48: 
  49:     //ラジオボタングループ name='underline' の値を取得
  50:     if (element.underline.value == 'on') {
  51:         attribute |= underline;
  52:     } else {
  53:         attribute &= ~underline;
  54:     }
  55: 
  56:     //属性値を表示
  57:     document.getElementById('attr').textContent = '属性値= 0b' + ('000' + attribute.toString(2)).slice(-3);
  58: 
  59:     //style属性を付与する:ボールド
  60:     if (Number(attribute & bold) != 0) {
  61:         document.getElementById('character').style.fontWeight = 'bold';
  62:     } else {
  63:         document.getElementById('character').style.fontWeight = 'normal';
  64:     }
  65: 
  66:     //style属性を付与する:イタリック
  67:     if (Number(attribute & italic) != 0) {
  68:         document.getElementById('character').style.fontStyle = 'italic';
  69:     } else {
  70:         document.getElementById('character').style.fontStyle = 'normal';
  71:     }
  72: 
  73:     //style属性を付与する:下線
  74:     if (Number(attribute & underline) != 0) {
  75:         document.getElementById('character').style.textDecoration = 'underline';
  76:     } else {
  77:         document.getElementById('character').style.textDecoration = 'none';
  78:     }
  79: }

まず、ボールドのON/OFFを指定するビットパターンを定数 bold に代入する。同様に、イタリックは italic に、下線は underline に代入する。文字装飾データ(3ビット値)を代入する変数は attribute とする。

次に、ラジオボタンの値を読み込んで、変数 attribute に値を設定する。
ボールドのラジオボタンがONなら、ビット論理和 | を使って変数 attribute にフラグを立てる。ここではビット論理和代入演算子 |= を使っている。
逆に、フラグを降ろすには、ビット論理積 & とビット否定演算子 ~ を組み合わせて使う。
ボールドに対応する1ビットの真理値表
attributeboldフラグを立てる
attribute | bold
~boldフラグを降ろす
attribute & ~bold
01100
11100
この表はボールドに対応する1ビットだけを示している。実際の ~bold は全32ビットを反転するため、~1 は -2 となる。対象以外のビットは1になるので、attribute & ~bold はボールドだけをOFFにし、他の装飾を保つ。
フラグの確認には (attribute & bold) !== 0、反転には attribute ^= bold を使う。比較演算子との優先順位による誤りを防ぐため、確認式のビット演算を括弧で囲もう。
このサンプルは操作のたびに0から値を組み立て直すので、OFF側の処理は省略しても結果は同じである。ここでは、既存の値からフラグを降ろす書き方も示している。

Numberのビット演算は32ビット

Number は整数専用の型ではなく、小数も扱う数値型である。ただし、Numberのビット演算は値を32ビットの整数に変換してから行う。小数部分は切り捨てられ、32ビットを超える上位の情報は失われる。たとえば 3.9 & 1 は1、(2 ** 32) 0 は0になる。
&、、^、~、<<、>> の結果は、符号付き32ビット整数(-2147483648~2147483647)である。>>> の結果は符号なし32ビット整数(0~4294967295)になる。
ビット論理積 &・ビット論理和 | と、条件を組み合わせる論理演算子 &&・|| は別物である。
32ビットを超える整数のビット演算には BigInt を使える。ただし、NumberとBigIntを混ぜて演算することはできず、BigIntには >>> がない。たとえば 1n << 40n は1099511627776nとなる。

シフト演算子

JavaScriptには、左シフト <<、符号を維持する右シフト >>、左側を0で埋める右シフト >>> がある。下表はNumberでの計算例である。2進数は読みやすいよう先頭の0を省略している。
シフト演算の例(Number)
式結果(2進数)結果(10進数)
0b01011100101110092
92 >> 21011123
23 >> 1101111
92 << 2101110000368
368 << 11011100000736
左シフトは右側を0で埋める。Numberでは8ビットではなく32ビットで演算するので、92 << 2 は112ではなく368である。32ビットの範囲からはみ出したビットは捨てられるため、左シフトが常に掛け算と同じ結果になるわけではない。
>> は左端の符号ビットを引き継ぐので、-8 >> 1 は -4 になる。>>> は左側を0で埋めるので、-8 >>> 1 は2147483644になる。正の整数を右に1ビットずらすと、2で割った商の小数部分を切り捨てた値になる。負数の >> は負の無限大方向への丸めとなり、-3 >> 1 は -2 である。
Numberのシフト数には下位5ビットだけを使う。たとえば 1 << 32 は 1 << 0 と同じ1になる。これはBigIntのシフトには当てはまらない。
それでは、CSSの色指定のうち #RRGGBB 形式(#に続く6桁の16進数)を例に、シフト演算を使ってみよう。R・G・Bは赤・緑・青の強さを表し、それぞれ0~255の8ビット、合計24ビットである。CSSには、このほかに3桁の短縮表記などもあるが、今回の入力は6桁に限定する。
このプログラムでは、色相環で反対側にある色を求める。具体的には、色を色相・彩度・明度で表す HSL の色相を180度回し、彩度と明度を保った色を「補色」と呼ぶ。これはRGBの各成分を255から引く「反転色」とは異なる。また、絵の具を混ぜる場合も含め、補色を混ぜれば必ず白になるという意味ではない。

この定義の補色は次の計算式で求められる。R0,G0,B0は元のカラーコード、R1,G1,B1は補色カラーコードを意味する。max(r,g,b)はr,g,bの最大値を、min(r,g,b)はr,g,bの最小値を求める関数である。 $$ \begin{alignedat}{2} \text{MAX} &= \max(R_0, G_0, B_0) \\ \text{MIN} &= \min(R_0, G_0, B_0) \\ C &= \text{MAX} + \text{MIN} \\ R_1 &= C - R_0 \\ G_1 &= C - G_0 \\ B_1 &= C - B_0 \end{alignedat} $$
補色計算
補色計算
bit2.html に48C253を入力すると、補色 #c248b7 が得られる。入力するのは6桁の16進数だけで、#は入力欄の外に表示している。

bit2.html

  25:     let input = document.getElementById('origin').value.trim(); //元のカラーコード
  26:     let origin; //入力検証に成功してから数値へ変換する
  27:     let ret = '';
  28: 
  29:     //バリデーション
  30:     let errmsg = '';
  31:     if (!/^[0-9a-fA-F]{6}$/.test(input)) {
  32:         errmsg = '6桁の16進数(0~9、a~f)を入力してください。#は不要です。';
  33:     }
  34: 
  35:     if (errmsg == '') {
  36:         origin = parseInt(input, 16);
  37:         //元のカラーコード
  38:         let red = origin & 0xFF0000;        //赤
  39:         red = red >> 16;                    //右シフトで8bit化
  40:         let green = origin & 0x00FF00;      //緑
  41:         green = green >> 8;                 //右シフトで8bit化
  42:         let blue = origin & 0x0000FF;       //青
  43:         //補色のカラーコード
  44:         let max = Math.max(red, green, blue);
  45:         let min = Math.min(red, green, blue);
  46:         let cc = min + max;
  47:         let red1   = cc - red;
  48:         let green1 = cc - green;
  49:         let blue1  = cc - blue;
  50:         let complement = (red1 << 16) | (green1 << 8) | blue1;  //左シフト
  51:         ret = '補色 #' + ('000000' + complement.toString(16)).slice(-6);

元のカラーコードは、RGBの順序に1バイト(8ビット)の値が入っている。そこで、まず最上位の8ビットをビット論理積を使ってマスクし、それを16ビットだけ右シフトさせることでRedの値にする。Green、Blueについても同様に操作する。Blueは最下位8ビットなので、右シフトさせる必要はない。
RGBの3つの値に分解できたところで、前述の計算式で補色のRGBを求める。
今度は分解したときとは逆に、左シフトさせていくことでRGBのカラーコードを組み立ててゆく。
先頭の0も6桁に含め、00ff00のように入力する。parseIntだけでは、12zzzzの先頭の12を読み取ってしまうため、変換前に正規表現で6桁すべてが16進数かを検証している。
赤 #ff0000 の補色は #00ffff である。一方、白・黒・灰色は彩度が0で色相を持たず、この式では元と同じ色になる。この計算だけで、読みやすい文字色や十分なコントラストが保証されるわけではない。

ビット演算子一覧

ビット演算子
演算子意味
a & bビット論理積(AND):両方のビットが1なら1
a | bビット論理和(OR):少なくとも一方が1なら1
a ^ bビット排他的論理和(XOR):ビットが異なれば1
~aビット否定(NOT):各ビットの0と1を反転
a << b左シフト
a >> b右シフト(符号維持)
a >>> b右シフト(ゼロ埋め)

コラム:IoTとビット演算、シフト演算

ワンボードマイコンのイラスト
ワンボードマイコンのイラスト
IoT(モノのインターネット)では、センサーから受け取ったデータを処理する。機器によっては、状態を示すフラグや測定値が1つのデータに詰め込まれており、必要なビットを取り出すときにビット演算が役立つ。データの並び方は機器の仕様書で確認する。
たとえば、12ビットの測定値を2バイトに分けて受け取り、上位バイトの下位4ビットと下位バイトを結合する場合、((high & 0x0f) << 8) low と書ける(highとlowは0~255)。
42ビットのように32ビットを超える値を、Numberのビット演算で一度に処理すると情報が失われる。バイトごとに処理するか、BigIntなどを使う必要がある。CやC++にもビット演算はあるが、型の幅や符号に関する規則はJavaScriptと同じではない。
乗算命令のないCPUでは、シフトと加算を組み合わせて整数の掛け算を実装できる。
たとえば $12 \times 35 = 12 \times (32 + 2 + 1)$ なので、JavaScriptでは (12 << 5) + (12 << 1) + 12 と書けば420になる。ただし、シフトで有効なビットが失われない範囲で使う必要がある。
右シフトは2のべき乗で割る処理に使えるが、一般の割り算には比較や減算なども必要である。通常のJavaScriptでは、読みやすい * や / を使おう。シフトに書き換えれば必ず高速になるわけではない。

コラム:機械語、コンパイラ、インタプリタ

IBM 704用FORTRANの解説書
機械語は、CPUが実行できる命令を数値で表したものである。命令の種類や表し方はCPUの設計によって異なる。
IBMのジョン・バッカスらは、科学者や技術者が数式に近い形でプログラムを書けるように FORTRAN を開発した。IBM 704用の最初のFORTRANコンパイラは1957年(昭和32年)に提供された。
高級言語は、CPUの個々の命令よりも、人間に分かりやすい形で処理を記述できる言語である。コンパイラはプログラムを別の言語や形式へ翻訳する。変換先は機械語とは限らず、仮想マシン向けのバイトコードなどの場合もある。

インタプリタは、プログラムや中間表現を読み取り、その意味に従って処理を実行する仕組みである。必ずソースコードを1行ずつ実行するわけではない。コンパイラとインタプリタは、速い・遅いという違いで定義されるものでもない。

LISPは1958年(昭和33年)に実装が始まったプログラミング言語であり、言語そのものをインタプリタと呼ぶのは正確ではない。LISPの処理系にはインタプリタもコンパイラもあり、自分自身の言語で評価器を記述できることでも知られる。JavaScriptでJavaScriptの処理系を書くことも可能であり、JS-Interpreterがその実例である。

現代のJavaScript処理系は、解釈実行とコンパイルを組み合わせる。たとえばV8は Ignition というインタプリタを持ち、実行中に機械語へ変換する JITコンパイル も利用する。JavaScriptを単に「インタプリタだから遅い」と決めつけることはできない。

eval は文字列をJavaScriptのコードとして評価する関数である。その有無が、計算できる問題の範囲やプログラムの移植性を決めるわけではない。移植性は、言語仕様や利用するAPIが移植先でも使えるかに左右される。万能チューリングマシンは計算の可能性を考える理論上のモデルであり、インタプリタや移植性の定義ではない。

参考サイト

(この項おわり)
header