Succinct なトライの実験 その 6

先日の修正(http://d.hatena.ne.jp/s-yata/20100117)に続いて,sumire-tries の select 関数を効率化しました.今回の変更点は,PopCount 関数の計算結果を有効利用するようにしたことです.

※ このエントリにおいて,PopCount 関数とは,整数を引数として受け取り,その整数をビット列と考えて,1 になっているビットの数を返す関数のことです.

よくある実装として,以下のようなものがあります.

※ UInt32 は 32 ビット符号なし整数のことを表しています.定数に U を付けるべきかとも思いましたが,分かりにくくなるので省略しました.

UInt32 PopCount(UInt32 unit)
{
	unit = ((unit & 0xAAAAAAAA) >> 1) + (unit & 0x55555555);
	unit = ((unit & 0xCCCCCCCC) >> 2) + (unit & 0x33333333);
	unit = ((unit >> 4) + unit) & 0x0F0F0F0F;
	unit += unit >> 8;
	unit += unit >> 16;
	return unit & 0x3F;  // ここがポイントになります.
}

最初に述べたように,PopCount 関数の主要な用途は,引数に含まれる 1 の数を求めることなので,これで何の問題もありません.でも,少しもったいないことをしています.

何がもったいないかというと,PopCount 関数の最後で切り捨てている上位 24 ビットのことです.この部分,実は,引数の上位 8, 16, 24 ビットに含まれる 1 の数が保存されています.そして,これらの情報を使うことによって,select 関数を少し効率化できます.

※ unit を 1 バイトずつに分割してみると,上位のバイトから順に,引数の上位 8, 16, 24, 32 ビットに含まれる 1 の数が保存されていることが分かります.

今回は,下位 8, 16, 24 ビットに含まれる 1 の数が欲しかったので,以下のように変更しました.

UInt32 PopCount(UInt32 unit)
{
	unit = ((unit & 0xAAAAAAAA) >> 1) + (unit & 0x55555555);
	unit = ((unit & 0xCCCCCCCC) >> 2) + (unit & 0x33333333);
	unit = ((unit >> 4) + unit) & 0x0F0F0F0F;
	unit += unit << 8;  // シフトを逆にしました.
	unit += unit << 16;
	return unit;  // 上位 8 ビット以外もまとめて返します.
}

基本的には,PopCount(x) >> 24 として,上位 8 ビットだけを取り出すことになりますが,必要に応じて,他の情報も引き出すことが可能です.

※ 同じ方法を前にもどこかで使ったはずなのですが,忘れていました.

sumire-tries の修正版は既にアップロードしてあります.

以前と同じように実験した結果は以下のようになりました.

ID コーパス キー数 平均キー長(bytes)
jawiki 日本語版 Wikipedia タイトル一覧 942,390 20.4
enwiki 英語版 Wikipedia タイトル一覧 6,437,567 18.6
ja1gms 日本語版 Google n-gram コーパスの 1-gram 2,565,427 18.8
en1gms 英語版 Google n-gram コーパスの 1-gram 13,588,391 8.3

次の表は,すべてのキーを辞書順に検索するのに要した時間(秒)を示しています.

ラベル昇順 jawiki enwiki ja1gms en1gms
BasicTrie 0.84 6.36 2.81 10.29
SuccinctTrie 3.49 33.34 12.71 57.14
LoudsTrie(旧) 4.00 27.67 12.04 37.87
LoudsTrie(新) 3.65 25.56 11.17 36.30
(新)/(旧) 91.3% 92.4% 92.8% 95.9%
LoudsPlusTrie(旧) 2.77 18.56 7.84 22.16
LoudsPlusTrie(新) 2.48 16.75 7.14 20.66
(新)/(旧) 89.5% 90.2% 91.1% 93.2%
出現頻度降順 jawiki enwiki ja1gms en1gms
BasicTrie 0.36 2.60 1.04 4.81
SuccinctTrie 0.77 5.52 2.40 13.05
LoudsTrie(旧) 2.50 16.34 6.70 20.91
LoudsTrie(新) 2.14 14.14 5.80 19.24
(新)/(旧) 85.6% 86.5% 86.6% 92.0%
LoudsPlusTrie(旧) 2.13 13.65 5.48 14.89
LoudsPlusTrie(新) 1.82 11.77 4.74 13.32
(新)/(旧) 85.4% 86.2% 86.5% 89.5%

次の表は,すべてのキーをランダム順に検索するのに要した時間(秒)を示しています.

ラベル昇順 jawiki enwiki ja1gms en1gms
BasicTrie 1.32 12.17 4.29 20.89
SuccinctTrie 5.61 67.47 21.09 123.48
LoudsTrie(旧) 5.99 51.48 17.54 56.50
LoudsTrie(新) 5.32 48.84 16.71 55.26
(新)/(旧) 88.8% 94.9% 95.3% 97.8%
LoudsPlusTrie(旧) 4.59 47.55 13.71 44.55
LoudsPlusTrie(新) 4.31 46.46 13.28 43.61
(新)/(旧) 93.9% 97.7% 96.9% 97.9%
出現頻度降順 jawiki enwiki ja1gms en1gms
BasicTrie 0.75 6.80 2.42 13.38
SuccinctTrie 2.35 23.07 7.06 40.69
LoudsTrie(旧) 4.41 39.85 12.54 39.56
LoudsTrie(新) 3.88 37.45 11.06 37.58
(新)/(旧) 88.0% 94.0% 88.2% 95.0%
LoudsPlusTrie(旧) 3.89 41.75 11.10 36.61
LoudsPlusTrie(新) 3.70 40.52 10.69 35.18
(新)/(旧) 95.1% 97.1% 96.3% 96.1%

先日の実験結果と比べると,検索時間が 2 〜 15% 短縮されています.前回と同様に,select の効率化は,日本語コーパスに対して高い効果を見せています.

他に注目すべき点は,ランダム順の検索において,LoudsTrie と LoudsPlusTrie で短縮率の差が大きいことです.原因としては,LoudsPlusTrie において,ビット列 2 本へのアクセスがボトルネックになっている可能性が考えられます.未検証ですが,実装次第(ビット列の統合)で LoudsPlusTrie は高速化できるかもしれません.