2
1

More than 1 year has passed since last update.

JavaScript でなるべく速くビットカウントする

Last updated at Posted at 2023-07-07

1 が立っているビット数を数えることを「ビットカウント」や "population count" 等と呼びます。

このときのビット数は 2 進数の「ハミング重み (hamming weight)」と一致します。

既に様々な高速なアルゴリズムが見つかっていますが、C 言語や Rust 等で高速だからと言って JavaScript でも高速とは限らないため、実際に実行時間を測ってみました。

JavaScript のビット演算では値を 32 ビットの整数として扱うため、ビットカウントも 32 ビットの整数について考えます。

参考「バイナリービット演算子 - 式と演算子 - JavaScript | MDN

1. 様々なアルゴリズム

おおよそ速い順に紹介します。

1.1. 分割統治法

ビットカウントの有名なアルゴリズムです (固有の名前は付いていない?) 。

環境によりますが、かなり速いです (具体的な実行時間の比較は後述します) 。

const popCountA = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const y = c * 0x01010101 >>> 24;
	return y;
};

const popCountB = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const y = c % 0xff;
	return y;
};

const popCountC = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const d = c + (c >>> 8);
	const e = d + (d >>> 16);
	const y = e & 0xff;
	return y;
};

const popCountD = x => {
	const a = (x & 0x55555555) + (x >>>  1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>>  2 & 0x33333333);
	const c = (b & 0x0f0f0f0f) + (b >>>  4 & 0x0f0f0f0f);
	const d = (c & 0x00ff00ff) + (c >>>  8 & 0x00ff00ff);
	const y = (d & 0x0000ffff) + (d >>> 16 & 0x0000ffff);
	return y;
};

const popCountE = x => {
	const a = x >>> 1 & 0o3_3333333333;
	const b = x - a - (a >>> 1 & 0o3_3333333333);
	// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
	const c = (b + (b >>> 3) & 0o3_0707070707) >>> 0;
	const y = c % 0o77;
	return y;
};

const popCountF = x => {
	const a = x >>> 1 & 0o3_3333333333;
	const b = x - a - (a >>> 1 & 0o3_3333333333);
	// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
	const c = (b + (b >>> 3) & 0o3_0707070707) >>> 0;
	const y = Number(BigInt(c) * 0o1_0101010101n >> 30n & 0o77n);
	return y;
};

1 ビットずつのカウント、2 ビットずつのカウント、4 ビットずつのカウント、8 ビットずつのカウント…というように分割統治法で計算します。

初めて見ると謎の定数が並んでいるように見えますが、定数を 2 進数にすると分かりやすいと思います。

0x55555555 === 0b01010101010101010101010101010101;
0x33333333 === 0b00110011001100110011001100110011;
0x0f0f0f0f === 0b00001111000011110000111100001111;
0x00ff00ff === 0b00000000111111110000000011111111;
0x0000ffff === 0b00000000000000001111111111111111;

popCountC 関数内の式を変形してできるのが popCountB および popCountA 関数になります。

※計算方法に関するより詳しい説明は後述します。

1.2. 計算済みのルックアップテーブルを作成する方法

あらかじめ値ごとにビットカウントして保存しておき、実際にビットカウントしたいときにルックアップテーブルを参照するだけにする方法です。

実際には 32 ビットの値全てに関してルックアップテーブルを作成するとメモリを消費しすぎるため、16 ビットや 8 ビットの値に関してルックアップテーブルを作成し、32 ビットの値を分割して計算します。

JavaScript では 32 ビットの値を配列のように単純に分割することができないため、ビット演算や DataView を用いて値を分割します。

1.2.1. 16 ビットに分割する場合

ほぼルックアップテーブルを参照するだけなので高速に動作しそうですが、TypedArray へのアクセスが遅い環境では前述の方法程は速くなりませんでした。

const POP_COUNTS_16 = new Uint8Array(0x10000);
for (let x = 0; x < POP_COUNTS_16.length; x++) {
	POP_COUNTS_16[x] = popCountA(x); // ここの計算は他のアルゴリズムでも良い
}

// ビット演算で値を分割する場合
const popCountG = x => POP_COUNTS_16[x & 0xffff] + POP_COUNTS_16[x >>> 16 & 0xffff];

// DataView で値を分割する場合
const aH = new Uint16Array(2);
const dataViewH = new DataView(aH.buffer);
const popCountH = x => {
	dataViewH.setUint32(0, x);
	return POP_COUNTS_16[aH[0]] + POP_COUNTS_16[aH[1]];
};

1.2.2. 8 ビットに分割する場合

速度的には 16 ビットに分割する方が 8 ビットに分割するより速いのは当たり前ですが、メモリ使用量は 8 ビットに分割する方が少なくできます。

const POP_COUNTS_8 = new Uint8Array(0x100);
for (let x = 0; x < POP_COUNTS_8.length; x++) {
	POP_COUNTS_8[x] = popCountA(x); // ここの計算は他のアルゴリズムでも良い
}

// ビット演算で値を分割する場合
const popCountI = x => (
	POP_COUNTS_8[x        & 0xff] +
	POP_COUNTS_8[x >>>  8 & 0xff] +
	POP_COUNTS_8[x >>> 16 & 0xff] +
	POP_COUNTS_8[x >>> 24 & 0xff]
);

// DataView で値を分割する場合
const aJ = new Uint8Array(4);
const dataViewJ = new DataView(aJ.buffer);
const popCountJ = x => {
	dataViewJ.setUint32(0, x);
	return POP_COUNTS_8[aJ[0]] + POP_COUNTS_8[aJ[1]] + POP_COUNTS_8[aJ[2]] + POP_COUNTS_8[aJ[3]];
};

1.3. WebAssembly の i32.popcnt 命令を使用する方法

WebAssembly にビットカウントするための命令 i32.popcnt があるため高速に動作するかと思いましたが、値のやり取りに時間がかかるのか、前述の方法程は速くなりませんでした。

popcnt.wat
(module
  (func (export "popcnt") (param i32) (result i32)
    local.get 0
    i32.popcnt
  )
)
popcnt.wasm を Base64 エンコードしたテキスト
AGFzbQEAAAABBgFgAX8BfwMCAQAHCgEGcG9wY250AAAKBwEFACAAaQsACgRuYW1lAgMBAAA=
const code = Uint8Array.from(
	atob('AGFzbQEAAAABBgFgAX8BfwMCAQAHCgEGcG9wY250AAAKBwEFACAAaQsACgRuYW1lAgMBAAA='),
	binaryChar => binaryChar.charCodeAt(0),
);
const { instance } = await WebAssembly.instantiate(code.buffer);
const { popcnt: popCountK } = instance.exports;

参考「Population count - WebAssembly | MDN
参考「wat2wasm demo
参考「[JavaScript] Unicode 文字列やバイナリデータを Base64 エンコードおよびデコードする - Qiita

1.4. 1 が立っているビットを 1 つずつ消していく方法

右 (下位) から 1 が立っているビットを 1 つずつ消していき、その回数を数えることでビットカウントする方法です。

1 が立っているビットが少ない場合は速いですが、32 ビットの値をまんべんなく受け取る可能性がある場合は期待値 (平均) 的にはかなり遅いです。

(ソースコードだけ見ると賢いやり方に見えますが、性能は良くないです。)

const popCountL = x => {
	let y = 0;
	for (let a = x; a !== 0; a &= a - 1) {
		y++;
	}
	return y;
};

a & a - 1 とすると 1 が立っている一番右のビットを 0 にできます。

0b10000001 & 0b10000001 - 1 === 0b10000001 & 0b10000000 === 0b10000000;
0b10001000 & 0b10001000 - 1 === 0b10001000 & 0b10000111 === 0b10000000;

2. 実行時間を比較

値によって実行時間が大きく異なるアルゴリズムがあるため、単純に考えると 0x00000000 から 0xffffffff まで計算したいですが、実行時間があまりにも長くなり過ぎる可能性があるため、等しい個数の乱数で測ります。

2.1. 実行時間

具体的なコードは後述します。

Google Chrome で確認した結果の平均は以下の表の通りです。

関数名 実行時間 備考
popCountX 48.133 ビットカウント以外の処理
popCountA 75.633 分割統治、乗算
popCountB 89.400 分割統治、剰余
popCountC 80.167 分割統治、減算、論理積
popCountD 92.667 分割統治
popCountE 90.700 分割統治、3 ビットずつ、剰余
popCountF 780.767 分割統治、3 ビットずつ、乗算、BigInt 変換
popCountG 97.333 ルックアップテーブル、16 ビット、ビット演算
popCountH 148.700 ルックアップテーブル、16 ビット、DataView
popCountI 108.800 ルックアップテーブル、8 ビット、ビット演算
popCountJ 160.667 ルックアップテーブル、8 ビット、DataView
popCountK 115.233 WebAssembly の i32.popcnt 命令
popCountL 554.633 1 が立っているビットを 1 つずつ数える

Deno (--allow-hrtime オプション使用) で確認した結果の平均は以下の表の通りです。

関数名 実行時間 備考
popCountX 60.295 ビットカウント以外の処理
popCountA 81.789 分割統治、乗算
popCountB 94.585 分割統治、剰余
popCountC 89.185 分割統治、減算、論理積
popCountD 99.545 分割統治
popCountE 98.218 分割統治、3 ビットずつ、剰余
popCountF 791.697 分割統治、3 ビットずつ、乗算、BigInt 変換
popCountG 74.178 ルックアップテーブル、16 ビット、ビット演算
popCountH 106.869 ルックアップテーブル、16 ビット、DataView
popCountI 88.058 ルックアップテーブル、8 ビット、ビット演算
popCountJ 122.055 ルックアップテーブル、8 ビット、DataView
popCountK 100.639 WebAssembly の i32.popcnt 命令
popCountL 563.477 1 が立っているビットを 1 つずつ数える

(Deno に別途ベンチマークの機能がありますが、ここでは Web ブラウザと同じ方法で比較しています。)

参考「now(): number - Performance | Runtime APIs | Deno

Deno の方が Google Chrome より TypedArray のアクセスが速いらしく、分割統治法とルックアップテーブルを作成する方法の実行時間の大小が逆転しました。

2.2. コード

ここでは以下のコードで実行時間を測ることにします。

JavaScript エンジンの最適化の影響を小さくするため、入力を乱数にし、ビットカウントの値全てを出力に影響させるようにしました。

// 実行時間を確認
const sleep = milliseconds => new Promise(resolve => { setTimeout(resolve, milliseconds); });

const bench = (name, fn) => {
	const beginTime = performance.now();
	fn();
	const endTime = performance.now();
	console.log(`${name}: ${endTime - beginTime}`);
};

for (let i = 0; i < 3; i++) {

	await sleep(1000);

	bench('popCountX', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= values[k];
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountA', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountA(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountB', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountB(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountC', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountC(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountD', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountD(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountE', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountE(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountF', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountF(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountG', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountG(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountH', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountH(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountI', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountI(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountJ', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountJ(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountK', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountK(values[k]);
			}
		}
		console.log(z);
	});

	await sleep(1000);

	bench('popCountL', () => {
		const values = new Uint32Array(0x4000);
		let z = 0;
		for (let j = 0; j < 0x800; j++) {
			crypto.getRandomValues(values);
			for (let k = 0; k < 0x4000; k++) {
				z ^= popCountL(values[k]);
			}
		}
		console.log(z);
	});

}

ちなみに以下のようにコードをまとめてしまうと最適化の影響か安定した結果が得られなくなるため、しない方が良さそうです。

悪い例
const popCounts = popCount => () => {
	const values = new Uint32Array(0x4000);
	let z = 0;
	for (let j = 0; j < 0x800; j++) {
		crypto.getRandomValues(values);
		for (let k = 0; k < 0x4000; k++) {
			z ^= popCount(values[k]);
		}
	}
	console.log(z);
};

bench('popCountA', popCounts(popCountA));
bench('popCountB', popCounts(popCountB));
bench('popCountC', popCounts(popCountC));
bench('popCountD', popCounts(popCountD));
bench('popCountE', popCounts(popCountE));
bench('popCountF', popCounts(popCountF));
bench('popCountG', popCounts(popCountG));
bench('popCountH', popCounts(popCountH));
bench('popCountI', popCounts(popCountI));
bench('popCountJ', popCounts(popCountJ));
bench('popCountK', popCounts(popCountK));
bench('popCountL', popCounts(popCountL));

3. 分割統治法によるビットカウントの説明

3.1. 基本の計算方法

3.1.1. 2 ビットずつから計算を始める場合

1 ビットずつのカウント、2 ビットずつのカウント、4 ビットずつのカウント、8 ビットずつのカウント…というように分割統治法で計算します。

8 ビットまでのビットカウントの例
1 1 1 1 1 1 1 1    1 1 1 1 1 1 1 1    11111111
  2   2   2   2     10  10  10  10    10101010
      4       4       0100    0100    01000100
              8           00001000    00001000

1 1 0 1 1 0 1 1    1 1 0 1 1 0 1 1    11011011
  2   1   1   2     10  01  01  10    10010110
      3       3       0011    0011    00110011
              6           00000110    00000110

0 1 0 0 1 0 0 1    0 1 0 0 1 0 0 1    01001001
  1   0   1   1     01  00  01  01    01000101
      1       2       0001    0010    00010010
              3           00000011    00000011

0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    00000000
  0   0   0   0     00  00  00  00    00000000
      0       0       0000    0000    00000000
              0           00000000    00000000

例えば、8 ビットまでのビットカウントは以下のコードによって計算できます。

各定数を 2 進数で表す場合
const a = (x & 0b01010101) + (x >>> 1 & 0b01010101);
const b = (a & 0b00110011) + (a >>> 2 & 0b00110011);
const y = (b & 0b00001111) + (b >>> 4 & 0b00001111);
各定数を 16 進数で表す場合
const a = (x & 0x55) + (x >>> 1 & 0x55);
const b = (a & 0x33) + (a >>> 2 & 0x33);
const y = (b & 0x0f) + (b >>> 4 & 0x0f);

同様に 32 ビットまでビットカウントすると、以下のコードになります。

const popCountD = x => {
	const a = (x & 0x55555555) + (x >>>  1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>>  2 & 0x33333333);
	const c = (b & 0x0f0f0f0f) + (b >>>  4 & 0x0f0f0f0f);
	const d = (c & 0x00ff00ff) + (c >>>  8 & 0x00ff00ff);
	const y = (d & 0x0000ffff) + (d >>> 16 & 0x0000ffff);
	return y;
};

各定数を 2 進数にすると以下のようになります。

0x55555555 === 0b01010101010101010101010101010101;
0x33333333 === 0b00110011001100110011001100110011;
0x0f0f0f0f === 0b00001111000011110000111100001111;
0x00ff00ff === 0b00000000111111110000000011111111;
0x0000ffff === 0b00000000000000001111111111111111;

3.1.2. 3 ビットずつから計算を始める場合

1 ビットずつのカウント、3 ビットずつのカウント、6 ビットずつのカウント…というように分割統治法で計算します。

8 ビットまでのビットカウントの例
1 1 1 1 1 1 1 1    1 1 1 1 1 1 1 1    11111111
  2     3     3     10   011   011    10011011
  2           6     10      000110    10000110
              8           00001000    00001000

1 1 0 1 1 0 1 1    1 1 0 1 1 0 1 1    11011011
  2     2     2     10   010   010    10010010
  2           4     10      000100    10000100
              6           00000110    00000110

0 1 0 0 1 0 0 1    0 1 0 0 1 0 0 1    01001001
  1     1     1     01   001   001    01001001
  1           2     01      000010    01000010
              3           00000011    00000011

0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    00000000
  0     0     0     00   000   000    00000000
  0           0     00      000000    00000000
              0           00000000    00000000

例えば、8 ビットまでのビットカウントは以下のコードによって計算できます。

各定数を 2 進数で表す場合
const a = (x & 0b01001001) + (x >>> 1 & 0b01001001) + (x >>> 2 & 0b01001001);
const b = (a & 0b11000111) + (a >>> 3 & 0b11000111);
const y = (b & 0b00111111) + (b >>> 6 & 0b00111111);
各定数を 8 進数で表す場合
const a = (x & 0o1_11) + (x >>> 1 & 0o1_11) + (x >>> 2 & 0o1_11);
const b = (a & 0o3_07) + (a >>> 3 & 0o3_07);
const y = (b & 0o0_77) + (b >>> 6 & 0o0_77);

※上記のコード中で下線 _ は見やすさのための数値の区切りとして書いているだけで、文法上必須なものではありません。
※ 9 ビットずつのビットカウントを計算することもできますが、後に式変形して演算の回数を減らせるようにするため 6 ビットずつのビットカウントを計算しています。

同様に 32 ビットまでビットカウントすると、以下のコードになります。

const a = (x & 0o1_1111111111) + (x >>> 1 & 0o1_1111111111) + (x >>> 2 & 0o1_1111111111);
// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
const b = (a & 0o3_0707070707) + (a >>> 3 & 0o3_0707070707) >>> 0;
const y = (
	(b        & 0o77) +
	(b >>>  6 & 0o77) +
	(b >>> 12 & 0o77) +
	(b >>> 18 & 0o77) +
	(b >>> 24 & 0o77) +
	(b >>> 30 & 0o77)
);

※ 12 ビットずつのビットカウントを計算することもできますが、後に式変形して演算の回数を減らせるようにするため 6 ビットずつのままビットカウントを計算しています。

各定数を 2 進数にすると以下のようになります。

0o1_1111111111 === 0b01_001001001001001001001001001001;
0o3_0707070707 === 0b11_000111000111000111000111000111;

0o77 === 0b111111;

3.2. 演算の回数を減らす

3.2.1. 2 ビットずつのビットカウントで減算を用いる

基本の計算方法では、2 ビットまでのビットカウントは以下のコードによって計算します。

const y = (x & 0b01) + (x >>> 1 & 0b01);

ここで、以下のような式変形をすることができます。

(x >>> 1 & 0b01) + (x >>> 1 & 0b01) === ((x >>> 1 & 0b01) << 1);
(x >>> 1 & 0b01) === ((x >>> 1 & 0b01) << 1) - (x >>> 1 & 0b01);

(
	(x & 0b01) + ((x >>> 1 & 0b01) << 1)
) === (
	(x & 0b01) + (x & 0b10)
) === (
	x
);

(
    (x & 0b01) + (x >>> 1 & 0b01)
) === (
	(x & 0b01) + ((x >>> 1 & 0b01) << 1) - (x >>> 1 & 0b01)
) === (
    x - (x >>> 1 & 0b01)
);

よって、2 ビットまでのビットカウントは以下のコードでも計算できます。

const y = x - (x >>> 1 & 0b01);

同様に考えると、32 ビットまでのビットカウント時の 2 ビットずつのビットカウントは以下のコードによって計算できます。

const a = x - (x >>> 1 & 0x55555555);

3.2.2. 8 ビットずつのビットカウントの論理積の計算をまとめる

基本の計算方法では、8 ビットまでのビットカウントは以下のコードによって計算します。

const y = (b & 0b00001111) + (b >>> 4 & 0b00001111);

ここで、y の最大値が 8 === 0b00001000 となり 4 ビットで表現できるため、論理積 & 0b00001111 によって 4 ビット切り出す計算をまとめることができます。

const y = b + (b >>> 4) & 0b00001111;

同様に考えると、32 ビットまでのビットカウント時の 8 ビットずつのビットカウントは以下のコードによって計算できます。

const c = b + (b >>> 4) & 0x0f0f0f0f;

ちなみに上記の式変形は「8 ビットまでのビットカウントを 4 ビットで表現できる」ために成り立ちます。

「2 ビットまでのビットカウントは 2 ビット」、「4 ビットまでのビットカウントは 3 ビット」が表現するために必要なため、同様の式変形はできません。

3.2.3. 16 ビットずつから 32 ビットまでのビットカウントの論理積の計算をまとめる

基本の計算方法では、16 ビットずつから 32 ビットまでのビットカウントは以下のコードによって計算します。

const d = (c & 0x00ff00ff) + (c >>>  8 & 0x00ff00ff);
const y = (d & 0x0000ffff) + (d >>> 16 & 0x0000ffff);

ここで、y の最大値が 32 === 0b00100000 より「16 ビットまでのビットカウント」も「32 ビットまでのビットカウント」も 8 ビット以下で表現できるため、論理積によって 8 ビット切り出す計算をまとめることができます。

const d = c + (c >>> 8);
const e = d + (d >>> 16);
const y = e & 0xff;

また、以下のコードでも同様に計算できます。

const y = c + (c >>> 8) + (c >>> 16) + (c >>> 24) & 0xff;

3.2.4. 16 ビットずつから 32 ビットまでのビットカウントで剰余を用いる

以下のように式変形すると、16 ビットずつから 32 ビットまでのビットカウントを剰余によって計算できることが分かります。

c0 = c        & 0xff;
c1 = c >>>  8 & 0xff;
c2 = c >>> 16 & 0xff;
c3 = c >>> 24 & 0xff;

c === c0 + (c1 << 8) + (c2 << 16) + (c3 << 24);

0x00000001 % 0xff === 1;
0x00000100 % 0xff === 1;
0x00010000 % 0xff === 1;
0x01000000 % 0xff === 1;

(
	c % 0xff
) === (
	(c0 + (c1 << 8) + (c2 << 16) + (c3 << 24)) % 0xff
) === (
	(c0 * 0x00000001 + c1 * 0x00000100 + c2 * 0x00010000 + c3 * 0x01000000) % 0xff
) === (
	(c0 + c1 + c2 + c3) % 0xff
) === (
	c0 + c1 + c2 + c3
) === (
	(c & 0xff) + (c >>> 8 & 0xff) + (c >>> 16 & 0xff) + (c >>> 24 & 0xff)
) === (
	c + (c >>> 8) + (c >>> 16) + (c >>> 24) & 0xff
);

よって、以下のように計算をまとめることもできます。

const y = c % 0xff;

3.2.5. 16 ビットずつから 32 ビットまでのビットカウントで乗算を用いる

論理積をまとめた式から、以下のような式変形をすることができます。

(
    c + (c >>> 8) + (c >>> 16) + (c >>> 24) & 0xff
) === (
    (c << 24) + (c << 16) + (c << 8) + c >>> 24 & 0xff
) === (
    c * 0x01010101 >>> 24 & 0xff
);

ここでは c の最大値が 0x08080808 のため、c * 0x01010101 の最大値は 0x08080808 * 0x01010101 === 0x00081018_20181008 となり 52 ビットの数になります。

JavaScript での通常の (Number 型の) 数は 53 ビットまでの正の整数を正しく扱えるため、問題なく計算できます。

参考「Number.MAX_SAFE_INTEGER - JavaScript | MDN

符号なし右シフト >>> は値を 32 ビットとして扱うため、32 ビットの値を右に 24 ビットシフトすることで 8 ビットの値しか残らないため、論理積 & 0xff を取り除くことができます。

(
    c * 0x01010101 >>> 24 & 0xff
) === (
    c * 0x01010101 >>> 24
);

よって、以下のように計算をまとめることもできます。

const y = c * 0x01010101 >>> 24;

3.2.6. 3 ビットずつのビットカウントで減算を用いる

基本の計算方法では、3 ビットまでのビットカウントは以下のコードによって計算します。

const y = (x & 0b001) + (x >>> 1 & 0b001) + (x >>> 2 & 0b001);

ここで、以下のような式変形をすることができます。

(x >>> 1 & 0b001) + (x >>> 1 & 0b001) === ((x >>> 1 & 0b001) << 1);
(x >>> 1 & 0b001) === ((x >>> 1 & 0b001) << 1) - (x >>> 1 & 0b001);

(x >>> 2 & 0b001) + (x >>> 2 & 0b001) + ((x >>> 2 & 0b001) << 1) === ((x >>> 2 & 0b001) << 2);
(x >>> 2 & 0b001) === ((x >>> 2 & 0b001) << 2) - ((x >>> 2 & 0b001) << 1) - (x >>> 2 & 0b001);

(
	(x & 0b001) + ((x >>> 1 & 0b001) << 1) + ((x >>> 2 & 0b001) << 2)
) === (
	(x & 0b001) + (x & 0b010) + (x & 0b100)
) === (
	x
);

(
	(x & 0b001) +
	(x >>> 1 & 0b001) +
	(x >>> 2 & 0b001)
) === (
	(x & 0b001) +
	((x >>> 1 & 0b001) << 1) - (x >>> 1 & 0b001) +
	((x >>> 2 & 0b001) << 2) - ((x >>> 2 & 0b001) << 1) - (x >>> 2 & 0b001)
) === (
	x - (x >>> 1 & 0b001) - ((x >>> 2 & 0b001) << 1) - (x >>> 2 & 0b001)
) === (
	x - (x >>> 1 & 0b001) - (x >>> 1 & 0b010) - (x >>> 2 & 0b001)
) === (
	x - (x >>> 1 & 0b011) - (x >>> 2 & 0b001)
) === (
	x - (x >>> 1 & 0b011) - ((x >>> 1 & 0b011) >>> 1 & 0b011)
);

よって、3 ビットまでのビットカウントは以下のコードでも計算できます。

const y = x - (x >>> 1 & 0b011) - ((x >>> 1 & 0b011) >>> 1 & 0b011);

ここで、一部の計算結果を変数に代入することで計算量をさらに減らせます。

const a = x >>> 1 & 0b011;
const y = x - a - (a >>> 1 & 0b011);

同様に考えると、32 ビットまでのビットカウント時の 3 ビットずつのビットカウントは以下のコードによって計算できます。

const a = x >>> 1 & 0o3_3333333333;
const b = x - a - (a >>> 1 & 0o3_3333333333);

3.2.7. 6 ビットずつのビットカウントの論理積の計算をまとめる

基本の計算方法では、6 ビットまでのビットカウントは以下のコードによって計算します。

const y = (b & 0b000111) + (b >>> 3 & 0b000111);

ここで、y の最大値が 6 === 0b000110 となり 3 ビットで表現できるため、論理積 & 0b000111 によって 3 ビット切り出す計算をまとめることができます。

const y = b + (b >>> 3) & 0b000111;

同様に考えると、32 ビットまでのビットカウント時の 6 ビットずつのビットカウントは以下のコードによって計算できます。

// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
const c = (b + (b >>> 3) & 0o3_0707070707) >>> 0;

ちなみに上記の式変形は「6 ビットまでのビットカウントを 3 ビットで表現できる」ために成り立ちます。

「3 ビットまでのビットカウントは 2 ビット」が表現するために必要なため、同様の式変形はできません。

3.2.8. 6 ビットずつから 32 ビットまでのビットカウントの論理積の計算をまとめる

基本の計算方法では、6 ビットずつから 32 ビットまでのビットカウントは以下のコードによって計算します。

const y = (
	(c        & 0o77) +
	(c >>>  6 & 0o77) +
	(c >>> 12 & 0o77) +
	(c >>> 18 & 0o77) +
	(c >>> 24 & 0o77) +
	(c >>> 30 & 0o77)
);

ここで、y の最大値が 32 === 0b100000 より「32 ビットまでのビットカウント」を 6 ビット以下で表現できるため、論理積によって 6 ビット切り出す計算をまとめることができます。

const y = c + (c >>> 6) + (c >>> 12) + (c >>> 18) + (c >>> 24) + (c >>> 30) & 0o77;

3.2.9. 6 ビットずつから 32 ビットまでのビットカウントで剰余を用いる

以下のように式変形すると、6 ビットずつから 32 ビットまでのビットカウントを剰余によって計算できることが分かります。

c0 = c        & 0o77;
c1 = c >>>  6 & 0o77;
c2 = c >>> 12 & 0o77;
c3 = c >>> 18 & 0o77;
c4 = c >>> 24 & 0o77;
c5 = c >>> 30 & 0o77;

c === c0 + (c1 << 6) + (c2 << 12) + (c3 << 18) + (c4 << 24) + (c5 << 30);

0o0_0000000001 % 0o77 === 1;
0o0_0000000100 % 0o77 === 1;
0o0_0000010000 % 0o77 === 1;
0o0_0001000000 % 0o77 === 1;
0o0_0100000000 % 0o77 === 1;
0o1_0000000000 % 0o77 === 1;

(
	c % 0o77
) === (
	(
		 c0        +
		(c1 <<  6) +
		(c2 << 12) +
		(c3 << 18) +
		(c4 << 24) +
		(c5 << 30)
	) % 0o77
) === (
	(
		c0 * 0o0_0000000001 +
		c1 * 0o0_0000000100 +
		c2 * 0o0_0000010000 +
		c3 * 0o0_0001000000 +
		c4 * 0o0_0100000000 +
		c5 * 0o1_0000000000
	) % 0o77
) === (
	(c0 + c1 + c2 + c3 + c4 + c5) % 0o77
) === (
	c0 + c1 + c2 + c3 + c4 + c5
) === (
	(c        & 0o77) +
	(c >>>  6 & 0o77) +
	(c >>> 12 & 0o77) +
	(c >>> 18 & 0o77) +
	(c >>> 24 & 0o77) +
	(c >>> 30 & 0o77)
) === (
	c + (c >>> 6) + (c >>> 12) + (c >>> 18) + (c >>> 24) + (c >>> 30) & 0o77
);

よって、以下のように計算をまとめることもできます。

const y = c % 0o77;

3.2.10. 6 ビットずつから 32 ビットまでのビットカウントで乗算を用いる

論理積をまとめた式から、理論上は以下のような式変形をすることができます。

理論上の式変形
(
    c + (c >>> 6) + (c >>> 12) + (c >>> 18) + (c >>> 24) + (c >>> 30) & 0o77
) === (
    (c << 30) + (c << 24) + (c << 18) + (c << 12) + (c << 6) + c >>> 30 & 0o77
) === (
    c * 0o1_0101010101 >>> 30 & 0o77
);

ここでは c の最大値が 0o2_0606060606 のため、c * 0o1_0101010101 の最大値は理論上は 0o2_0606060606 * 0o1_0101010101 === 0o2_1016243240_3630221406 となり 62 ビットの数になります。

JavaScript での通常の (Number 型の) 数で正しく扱える正の整数は 53 ビットまでのため、計算で問題が発生します。

実際に JavaScript で計算すると 0o2_0606060606 * 0o1_0101010101 === 0o2_1016243240_3630222000 になります。

参考「Number.MAX_SAFE_INTEGER - JavaScript | MDN

Math.imul() 関数を使用することで 32 ビットまでの乗算を行うことができますが、ここでは 36 ビットまでの値を計算に用いるため、使えません。

参考「Math.imul() - JavaScript | MDN

よって、正しく計算するためには BigInt を使う必要があります。

参考「BigInt - JavaScript | MDN

ただし BigInt は常に符号ありとして扱われるため、符号なし右シフト >>> を使用することはできません。

参考「演算子 - BigInt - JavaScript | MDN

よって、以下のように計算をまとめることもできます。

const y = Number(BigInt(c) * 0o1_0101010101n >> 30n & 0o77n);

ちなみに最長 62 ビットの値を右に 30 ビットシフトすることで 32 ビットの値が残る可能性があるため、論理積 & 0o77n を取り除くことはできません。

3.3. 演算の回数を減らした計算方法

実際に演算の回数を減らす式変形をすると、以下のようなコードで 32 ビットまでのビットカウントを行えます。

const popCountA = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const y = c * 0x01010101 >>> 24;
	return y;
};

const popCountB = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const y = c % 0xff;
	return y;
};

const popCountC = x => {
	const a = x - (x >>> 1 & 0x55555555);
	const b = (a & 0x33333333) + (a >>> 2 & 0x33333333);
	const c = b + (b >>> 4) & 0x0f0f0f0f;
	const d = c + (c >>> 8);
	const e = d + (d >>> 16);
	const y = e & 0xff;
	return y;
};
const popCountE = x => {
	const a = x >>> 1 & 0o3_3333333333;
	const b = x - a - (a >>> 1 & 0o3_3333333333);
	// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
	const c = (b + (b >>> 3) & 0o3_0707070707) >>> 0;
	const y = c % 0o77;
	return y;
};

const popCountF = x => {
	const a = x >>> 1 & 0o3_3333333333;
	const b = x - a - (a >>> 1 & 0o3_3333333333);
	// メモ: JavaScript でのビット演算は基本的に符号ありで計算されるため、符号なしの数に変換する
	const c = (b + (b >>> 3) & 0o3_0707070707) >>> 0;
	const y = Number(BigInt(c) * 0o1_0101010101n >> 30n & 0o77n);
	return y;
};
2
1
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1