SlideShare a Scribd company logo
1 of 17
Trie Tree ならびに Double Array Trie Tree を用いた文字列
処理パフォーマンスの検証
1
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
 記事データ
 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
○ 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
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
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
○ 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
こちらについても、全般的に 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
 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
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
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
なものの 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
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
で上回っており、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
分割なし 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
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
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

More Related Content

More from moai kids

Casual Compression on MongoDB
Casual Compression on MongoDBCasual Compression on MongoDB
Casual Compression on MongoDBmoai kids
 
Introduction to MongoDB
Introduction to MongoDBIntroduction to MongoDB
Introduction to MongoDBmoai kids
 
Hadoop Conference Japan 2011 Fallに行ってきました
Hadoop Conference Japan 2011 Fallに行ってきましたHadoop Conference Japan 2011 Fallに行ってきました
Hadoop Conference Japan 2011 Fallに行ってきましたmoai kids
 
HBase本輪読会資料(11章)
HBase本輪読会資料(11章)HBase本輪読会資料(11章)
HBase本輪読会資料(11章)moai kids
 
snappyについて
snappyについてsnappyについて
snappyについてmoai kids
 
第四回月次セミナー(公開版)
第四回月次セミナー(公開版)第四回月次セミナー(公開版)
第四回月次セミナー(公開版)moai kids
 
第三回月次セミナー(公開版)
第三回月次セミナー(公開版)第三回月次セミナー(公開版)
第三回月次セミナー(公開版)moai kids
 
Pythonで自然言語処理
Pythonで自然言語処理Pythonで自然言語処理
Pythonで自然言語処理moai kids
 
HandlerSocket plugin Client for Javaとそれを用いたベンチマーク
HandlerSocket plugin Client for Javaとそれを用いたベンチマークHandlerSocket plugin Client for Javaとそれを用いたベンチマーク
HandlerSocket plugin Client for Javaとそれを用いたベンチマークmoai kids
 
Yammer試用レポート(公開版)
Yammer試用レポート(公開版)Yammer試用レポート(公開版)
Yammer試用レポート(公開版)moai kids
 
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)掲示板時間軸コーパスを用いたワードトレンド解析(公開版)
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)moai kids
 
中国と私(仮題)
中国と私(仮題)中国と私(仮題)
中国と私(仮題)moai kids
 
不自然言語処理コンテストLT資料
不自然言語処理コンテストLT資料不自然言語処理コンテストLT資料
不自然言語処理コンテストLT資料moai kids
 
n-gramコーパスを用いた類義語自動獲得手法について
n-gramコーパスを用いた類義語自動獲得手法についてn-gramコーパスを用いた類義語自動獲得手法について
n-gramコーパスを用いた類義語自動獲得手法についてmoai kids
 
Analysis of ‘lang-8’
Analysis of ‘lang-8’Analysis of ‘lang-8’
Analysis of ‘lang-8’moai kids
 
Androidの音声認識とテキスト読み上げ機能について
Androidの音声認識とテキスト読み上げ機能についてAndroidの音声認識とテキスト読み上げ機能について
Androidの音声認識とテキスト読み上げ機能についてmoai kids
 
Amebaサーチ使用傾向
Amebaサーチ使用傾向Amebaサーチ使用傾向
Amebaサーチ使用傾向moai kids
 
Amebaサーチのデータを用いた応用
Amebaサーチのデータを用いた応用Amebaサーチのデータを用いた応用
Amebaサーチのデータを用いた応用moai kids
 
Javaにおけるデータシリアライズと圧縮
Javaにおけるデータシリアライズと圧縮Javaにおけるデータシリアライズと圧縮
Javaにおけるデータシリアライズと圧縮moai kids
 
Amebaにおける絵文字
Amebaにおける絵文字Amebaにおける絵文字
Amebaにおける絵文字moai kids
 

More from moai kids (20)

Casual Compression on MongoDB
Casual Compression on MongoDBCasual Compression on MongoDB
Casual Compression on MongoDB
 
Introduction to MongoDB
Introduction to MongoDBIntroduction to MongoDB
Introduction to MongoDB
 
Hadoop Conference Japan 2011 Fallに行ってきました
Hadoop Conference Japan 2011 Fallに行ってきましたHadoop Conference Japan 2011 Fallに行ってきました
Hadoop Conference Japan 2011 Fallに行ってきました
 
HBase本輪読会資料(11章)
HBase本輪読会資料(11章)HBase本輪読会資料(11章)
HBase本輪読会資料(11章)
 
snappyについて
snappyについてsnappyについて
snappyについて
 
第四回月次セミナー(公開版)
第四回月次セミナー(公開版)第四回月次セミナー(公開版)
第四回月次セミナー(公開版)
 
第三回月次セミナー(公開版)
第三回月次セミナー(公開版)第三回月次セミナー(公開版)
第三回月次セミナー(公開版)
 
Pythonで自然言語処理
Pythonで自然言語処理Pythonで自然言語処理
Pythonで自然言語処理
 
HandlerSocket plugin Client for Javaとそれを用いたベンチマーク
HandlerSocket plugin Client for Javaとそれを用いたベンチマークHandlerSocket plugin Client for Javaとそれを用いたベンチマーク
HandlerSocket plugin Client for Javaとそれを用いたベンチマーク
 
Yammer試用レポート(公開版)
Yammer試用レポート(公開版)Yammer試用レポート(公開版)
Yammer試用レポート(公開版)
 
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)掲示板時間軸コーパスを用いたワードトレンド解析(公開版)
掲示板時間軸コーパスを用いたワードトレンド解析(公開版)
 
中国と私(仮題)
中国と私(仮題)中国と私(仮題)
中国と私(仮題)
 
不自然言語処理コンテストLT資料
不自然言語処理コンテストLT資料不自然言語処理コンテストLT資料
不自然言語処理コンテストLT資料
 
n-gramコーパスを用いた類義語自動獲得手法について
n-gramコーパスを用いた類義語自動獲得手法についてn-gramコーパスを用いた類義語自動獲得手法について
n-gramコーパスを用いた類義語自動獲得手法について
 
Analysis of ‘lang-8’
Analysis of ‘lang-8’Analysis of ‘lang-8’
Analysis of ‘lang-8’
 
Androidの音声認識とテキスト読み上げ機能について
Androidの音声認識とテキスト読み上げ機能についてAndroidの音声認識とテキスト読み上げ機能について
Androidの音声認識とテキスト読み上げ機能について
 
Amebaサーチ使用傾向
Amebaサーチ使用傾向Amebaサーチ使用傾向
Amebaサーチ使用傾向
 
Amebaサーチのデータを用いた応用
Amebaサーチのデータを用いた応用Amebaサーチのデータを用いた応用
Amebaサーチのデータを用いた応用
 
Javaにおけるデータシリアライズと圧縮
Javaにおけるデータシリアライズと圧縮Javaにおけるデータシリアライズと圧縮
Javaにおけるデータシリアライズと圧縮
 
Amebaにおける絵文字
Amebaにおける絵文字Amebaにおける絵文字
Amebaにおける絵文字
 

Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

  • 1. Trie Tree ならびに Double Array Trie Tree を用いた文字列 処理パフォーマンスの検証 1
  • 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