Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証
- 2. 1 はじめに
ブログなどの大量のテキストデータが蓄積される CGM サイトでは、必然的にシステムや
プログラムの中でも文字列処理の回数が多くなる。そのため効率的なアルゴリズムの採用
は、サイトのプログラム処理速度、ひいては対ユーザのページレスポンスタイムに関わる
重要な事案となる。
今回は、文字列の全文一致検索や前方一致検索処理において効率的なアルゴリズムとして
知られる「トライ木」についてそのパフォーマンスの検証を行った。
2 TrieTree について
2.1 アルゴリズムの内容
以下、トライ木についての解説を wikipedia より引用する。
トライ木(Trie)とは、順序付き木構造 (データ構造)の一種。プレフィックス木
(Prefix Tree)とも呼ばれる。キーが文字列である連想配列の実装構造として使
われる。2 分探索木と異なり、各ノードに個々のキーが格納されるのではなく、
木構造上のノードの位置とキーが対応している。あるノードの配下の全ノードは、
自身に対応する文字列に共通するプレフィックス(接頭部)があり、ルート
(根)には空の文字列が対応している。値は一般に全ノードに対応して存在する
わけではなく、末端ノードや一部の中間ノードだけがキーに対応した値を格納し
ている。[1]
文字列を、先頭文字から一文字ずつを節とする木構造として扱うのがトライ木、といえる。
2.2 一般的に言われるメリット
一般的に、以下がトライ木のメリットと言われている。
キー検索が高速
文字列を格納する際にメモリを節約できる。
前方一致文字の検索ならびに完全一致文字の検索に非常に適している。
木構造としてバランスが取れてなくてもよい(ルートノードの下のノードの深さ、
数がバラバラでも性能に影響が少ない)
2.3 得意な処理
上記のメリットから、ある文章から完全一致する語を抽出する処理や、Common Prefix
Search(ある語の集団から、検索キーと前方一致する語をすべて抽出するアルゴリズム )を
実装するのに適している。
また、この特性から、多くの辞書アプリやスペルチェッカーなどにトライ木のアルゴリズ
ムは採用されている。
以下、Java 標準 API との比較により確認する。
3 比較(Trie Tree × Java 標準 API)
以下のデータを元に、Trie Tree と Java 標準 API(String#indexOf, String#startsWith)と
のパフォーマンスの比較を行った。
なお記事データはファイルから1行ずつ逐次読み込み、辞書データはメモリ上にすべて保
管した上で計測を行った。
測定結果は、同試験を 10 回試行し、最大と最小の値を除いた平均値を算出した。
2
- 3. 記事データ
1,000 記事
10,000 記事
100,000 記事
辞書データ
100 語
1,000 語
10,000 語
検証マシンのスペック
Macbook Air
CPU:1.6GHz Core 2 Duo
Memory:2GB
3.1 処理速度
3.1.1 完全一致
記事データから、辞書に含まれている単語と完全一致する語を抽出する。
Java 標準 API では String#indexOf を使用し検証を行った。
○ 1,000 記事
TrieTree Java API
100 語 77.125 142.625
1000 語 60.25 487.5
10000 語 108.125 5440.25
1000記事
0
1000
2000
3000
4000
5000
6000
1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
3
- 4. ○ 10,000 記事
TrieTree Java API
100 語 529.75 600.375
1000 語 690.375 5018
10000 語 762.75 84526.13
10000記事
0
10000
20000
30000
40000
50000
60000
70000
80000
90000
10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
○ 100,000 記事
TrieTree Java API
100 語 4680.5 7877.25
1000 語 6824.625 69801.63
10000 語 8438.25 766336
4
- 5. 100000記事
0
100000
200000
300000
400000
500000
600000
700000
800000
900000
100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
全般的に TrieTree の方が処理速度が速いことが分かる。
記事数が増えるごとのパフォーマンスの劣化の度合いは TrieTree は「log(n)」であ
るのに対し、Java APIを使用したものはほぼ「n」となっている。こちらはそれぞ
れのアルゴリズムの特性と一致しているため、本計測結果も妥当な結果が得られていると
思われる。
3.1.2 .Common Prefix Search
記事データから、辞書に含まれている単語の Common Prefix Search の結果を抽出する。
Java 標準 API では、String#startsWith を使用して検証を行った。
○ 1,000 記事
Trie Tree Java API
100 語 14.625 21.625
1000 語 15.625 56.875
10000 語 12.625 888.5
5
- 6. 1000記事
0
100
200
300
400
500
600
700
800
900
1000
1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
○ 10,000 記事
Trie Tree Java API
100 語 117.25 162
1000 語 130.375 500.75
10000 語 116 7986.75
10000記事
0
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
6
- 7. ○ 100,000 記事
Trie Tree Java API
100 語 2308.5 1649.75
1000 語 2129.25 5022.875
10000 語 2125.375 109272
100000記事
0
20000
40000
60000
80000
100000
120000
100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語
ms
Trie Tre e
J ava API
n
log(n)
7
- 8. こちらについても、全般的に Trie Tree の方が処理が速いことが分かる。
完全一致の際は Trie Tree のパフォーマンス劣化度合いは「log(n)」であったのに対し、
Common Prefix Search については辞書語数によるパフォーマンスの差がほとんど現れな
い。
先頭文字に一致する語を辞書から走査するアルゴリズムのため、ノード探索数の増加が辞
書の語数にそれほど比例しないことが理由と考えられる。
3.2 メモリ効率
10,000 語の辞書データを用いて、Trie Tree と Java 標準 API のメモリ使用効率について調
査した。なお、Java 標準 API 側については、配列 ArrayList に String 型のインスタンス
を詰める形で検証を行った。
メモリ使用サイズの検証は、GC が発生しない環境でそれぞれのインスタンスを生成し、
その前後でのメモリ空き領域の差から値を算出した。
測定結果は、同試験を 10 回試行し、最大と最小の値を除いた平均値を算出した。
Trie Tree Java API
3222423
2
1610642
4
一般的には Trie Tree の方がメモリ効率が良いといわれているが、今回の検証では逆の結
果となっている。原因としては、今回の実装では Trie Tree を表現するために複数のイン
スタンスを組み合わせる形となっており、Java 標準 API に比してメモリ使用のオーバー
ヘッドが多めの実装となっていることが原因と思われる。
当たり前の話ではあるが、アルゴリズム的に省メモリが謳われていても、実装が富豪プロ
グラミングになっていればメモリ効率が劣化することの証明でもある。
Trie Tree 実装を省メモリ構造に作り変えての検証は、今回は行わない。
4 Double Array Trie Tree について
Trie 木アルゴリズムの改良版で最近有名となっている Double Array Trie Tree についても
検証を行った。
Double Array Trie Tree は、1989 年に J.-I. Aoe 氏によって提唱されたアルゴリズムである。
[2]
近年では、工藤拓氏が開発した形態素解析器の「MeCab」にて辞書データを表現する箇所
に採用されているのが有名である。[3]
4.1 アルゴリズムの内容
Double Array Trie Tree は、要素に数値をもつ二つの配列を使用することで Trie 木を表現
するアルゴリズムである。以下に挙げるルールにより実装される。
配列は「BASE」と「CHECK」の二つの整数値をもっている。(この配列を BC と
する)
文字を整数にマッピングするテーブル CODE により、対象文字と BC 配列のマッピ
ングをする。(このマッピングテーブルを CODE とする)
節 x において文字 c に対応する枝が存在し、その枝をたどることで y に辿りつくこ
とが出来る時
BC[x].BASE + CODE[c] = y
BC[y].CHECK = x
が成り立つ
文字の終端には
8
- 9. BASE=(マイナスの値)
を設定する。
Trie 木を Double Array Trie Tree の構造に変換する様を図示した例として、以下の図を
引用する。上部が Trie 木で、下部が Double Array Trie Tree の構造である。
[4]
4.2 Trie 木と比較した一般的に言われるメリット/デメリットについて
一般的に以下のように言われている。
4.2.1 メリット
文字列の木構造を数値のみで表現できるため、検索速度が高速。
同様の理由で、木構造データのサイズをコンパクトにできる
4.2.2 デメリット
Trie 木の構造を配列に変換する処理に時間がかかる。
マッピングテーブルのサイズが大きくなる。
若干構造が複雑なため、実装は煩雑なものになる。
特に、ノードの動的追加についてはノード再構築を伴うため実用的なパフォーマン
スを出すのは簡単ではないとされている。
5 比較(Trie Tree × Double Array Trie Tree)
3.の Trie Tree×Java 標準 API での比較と同様の条件での比較を、Trie Tree と Double
Array Trie Tree においても実施した。
記事データはファイルから1行ずつ逐次読み込み、辞書データはメモリ上にすべて保管し
た上で計測を行った。
測定結果は、同試験を 10 回試行し、最大と最小の値を除いた平均値を算出した。
なお、Trie Tree の計測結果については3.で実施したものをそのまま使用している。
9
- 10. 5.1 処理速度
5.1.1 完全一致
○ 1,000 記事
Trie Tree DATT
100 語 77.125 138.75
1000 語 60.25 140.25
10000 語 108.125 178.75
1000記事
0
20
40
60
80
100
120
140
160
180
200
1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語
ms
Trie Tre e
DATT
○ 10,000 記事
Trie Tree DATT
100 語 529.75 1038.625
1000 語 690.375 1453.375
10000 語 762.75 1865.625
10
- 11. 10000記事
0
200
400
600
800
1000
1200
1400
1600
1800
2000
10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語
ms
Trie Tre e
DATT
○ 100,000 記事
Trie Tree DATT
100 語 4680.5 16426
1000 語 6824.625 26766.63
10000 語 8438.25 61353
100000記事
0
10000
20000
30000
40000
50000
60000
70000
100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語
ms
Trie Tre e
DATT
残念ながら期待したような結果は出ず、Java 標準 API を採用した場合にくらべると優秀
11
- 12. なものの Double Array Trie Tree の方が単純 Trie 木に比べてパフォーマンスが劣化してい
る。アルゴリズムを実装する上で Java のインスタンスをいくつか生成する形になってい
るため、生成やハンドリングのオーバーヘッドが大きくなっていることが原因と考えられ
る。
また、辞書データが多くなるにしたがって処理の劣化度合いが高まるのは、マッピングテ
ーブルや BC 配列の大型化による探索コストの増大が考えられる。
後者については、辞書データの分割によりどの程度のパフォーマンスの差が生じるか、後
段で別途検証する。
Trie と同様 Double Array Trie Tree もパフォーマンスの劣化度合いは基本的に log(n)に殉
じている。
5.1.2 Common Prefix Search
○ 1,000 記事
Trie Tree DATT
100 語 14.625 28.875
1000 語 15.625 19.625
10000 語 12.625 19.75
1000記事
0
5
10
15
20
25
30
35
1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語
ms
Trie Tre e
DATT
○ 10,000 記事
Trie Tree DATT
100 語 117.25 193.125
1000 語 130.375 203.875
10000 語 116 148
12
- 13. 10000記事
0
50
100
150
200
250
10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語
ms
Trie Tre e
DATT
○ 100,000 記事
Trie Tree DATT
100 語 2308.5 1708.625
1000 語 2129.25 1702.625
10000 語 2125.375 1626.75
100000記事
0
500
1000
1500
2000
2500
100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語
ms
Trie Tre e
DATT
Common Prefix Search については、100,000 記事での検証にて Trie 木をパフォーマンス
13
- 14. で上回っており、Trie 木に対する優位性があることが確認できた。
Common Prefix Search でパフォーマンスが発揮できた原因としては、ルートノード直下
の節が決まればその配下の節の探索だけで済むため、辞書データが大きくなった結果節の
数が増大しても節選択のオーバーヘッドが比較的少ないから、と考えられる。
また完全一致探索のアルゴリズム実装に問題があるとした場合、節探索のオーバーヘッド
が大きい実装になっている可能性があり、改善の余地がある。
5.2 メモリ効率
10,000 語の辞書データを用いて、Trie Tree、Double Array Trie Tree ならびに Java 標準
API のメモリ使用効率について調査した。
Trie ならびに Java 標準 API については、測定結果は3.の結果をそのまま採用した。
メモリ使用サイズの検証は、GC が発生しない環境でそれぞれのインスタンスを生成し、
その前後でのメモリ空き領域の差から値を算出した。
測定結果は、同試験を 10 回試行し、最大と最小の値を除いた平均値を算出した。
Trie Tree DATT Java API
3222423
2
1207989
6
16114992
3者の中で Double Array Trie Tree のメモリ効率がもっとも優秀であることが分かる。
一般的にメリットと言われている木構造のコンパクトさが実証された結果となっている。
6 Double Array Trie Tree の問題について
6.1 Tree 構築にかかる時間
4.2.のデメリットにも記載したとおり、Double Array Trie Tree の既知の問題として Tree
構築に時間がかかることが挙げられ、ノードの数や節の数の増大により指数関数的に処理
時間が増大する。
こちらを回避するアプローチとしてはいくつか考えられるが、今回はひとつの辞書を複数
の Double Array Trie Tree に分割して構築するアプローチを採用し、どの程度構築時間が
短縮できるか、検証を行った。
辞書データ
100 語
1,000 語
10,000 語
分割単位
1つ
16 分割
先頭文字の下位1バイトの値によりクラスタリング
28 分割
16 分割に加え、アスキー文字のみ、ひらがなのみ、カタカナ 10 分割(ア
行、カ行・・・ワ行)にて分割
結果は以下になる。
14
- 15. 分割なし 16 分割 28 分割
100 語 365 384 224
1000 語 5848 2444 2071
10000 語 546164 33802 30544
辞書作成時間比較
0
100000
200000
300000
400000
500000
600000
100語 1000語 10000語
ms
分割なし
16分割
28分割
分割なしの場合、単語数が 10000 を超えた段階で爆発的に処理時間が劣化しているが、分
割することにより劣化を和らげることができている。
辞書構築の時間短縮には、辞書の分割が効果的であることが実証できた。
6.2 辞書データが多い時の探索時間
Double Array Trie Tree の構造的な問題とはいえないかもしれないが、5.の検証によりノ
ード数・節数が増えることによりパフォーマンス劣化が発生していたため、6.1.のアプロ
ーチにて辞書分割を行うことでパフォーマンスの改善が行われるかどうか、検証した。
記事データ
100,000 記事
辞書データ
100 語
1,000 語
10,000 語
辞書の分割単位
分割なし
16分割
28分割
15
- 16. 6.2.1 完全一致
分割なし 16 分割 28 分割
100 語 16426 13098.5 24158.5
1000 語 26766.63 17964.38 27041.63
10000 語 61353 44077.5 71881.88
100000記事
0
10000
20000
30000
40000
50000
60000
70000
80000
100語 1000語 10000語
ms
分割なし
16分割
28分割
6.2.2 Common Prefix Search
分割なし 16 分割 28 分割
100 語 1708.625 1508.25 1379
1000 語 1702.625 1423.125 1354.375
10000 語 1626.75 1414 1346.125
16
- 17. 100000記事
0
200
400
600
800
1000
1200
1400
1600
1800
100語 1000語 10000語
ms
分割なし
16分割
28分割
完全一致、Common Prefix Search とも、分割単位を増やすことでパフォーマンスの劣化
が緩和されていることが分かる。また、28分割時の完全一致のように、分割数を増やし
すぎると逆にパフォーマンスが落ちることも確認できた。
Double Array Trie Tree に汎用的に言えることかどうかは確証が持てないが、少なくとも
今回実装したロジックにおいては辞書データの適切な分割はパフォーマンスの向上に寄与
することが証明できた。
7 考察とまとめ
上記の結果をふまえ、文字列走査における Trie 木アルゴリズムの有効性が確認できた。
また、今回用意した実装には若干の問題がある可能性があるが、アルゴリズムとして
Double Array Trie Tree のパフォーマンスならびにメモリ効率の優秀性が確認できた。
また、Double Array Trie Tree の辞書データ作成や文字列検索のパフォーマンス向上に辞
書データの分割が有効であることを確認できた。
今後は、Double Array Trie Tree の Java 実装のさらなるブラッシュアップを行っていきた
いと考えている。
アルゴリズムの改善策として、分岐のない節を一緒くたにまとめ TAIL という配列で管理
する手法[5]など様々な手法が提唱されているため、それらを比較検証した上で取り入れて
いきたいと思う。
また、Double Array Trie Tree は動的なノード追加の実装難易度が高いことが問題である
が、この点を解消するための実装アルゴリズムや検証なども取り組み甲斐のある課題と思
われる。
8 参考文献・URL
[1] http://ja.wikipedia.org/wiki/%E3%83%88%E3%83%A9%E3%82%A4%E6%9C%A8
[2] http://www2.computer.org/portal/web/csdl/abs/trans/ts/1989/09/e1066abs.htm
[3] http://mecab.sourceforge.net/
[4] http://nanika.osonae.com/DArray/dary.html のサイトから図を引用
[5] http://linux.thai.net/~thep/datrie/datrie.html#Suffix
17