大量のテキストからトークンを切り出す場合(形態素解析など)に、同じ内容のトークンが大量に発生することがあります。
このトークンが不変なオブジェクトであるならば、同じ内容のトークンは1つのオブジェクトを使いまわす、すなわちキャッシュすることによって、メモリ使用量を減らせそうだなということで試してみました。
コード
Javaでの検証となります。 アルファベット以外を区切り文字とし、小文字にしたアルファベットの文字列をトークンとするといった簡易的なコードで検証します。
キャッシュなしは下記のようなコードになります。
public class Tokenizer { public List<String> tokenize(String text) { String[] tokens = text.split("[^a-zA-Z]+"); return Stream.of(tokens) .map(String::toLowerCase) .collect(Collectors.toList()); } }
キャッシュありでは、トークンのキャッシュのために、HashMapを使用します。Key、Valueともにトークンが入ります。(マルチスレッドで使用されることを想定していないコードになっているので注意)
public class CachedTokenizer { private HashMap<String, String> tokenCache = new HashMap<>(); public List<String> tokenize(String text) { String[] tokens = text.split("[^a-zA-Z]+"); return Stream.of(tokens) .map(String::toLowerCase) .map(token -> tokenCache.computeIfAbsent(token, key -> key)) .collect(Collectors.toList()); } }
検証結果
英文のテキストを使用して検証を行いました。 言語処理100本ノック 2015 にて配布されている下記コーパスの先頭50万行を使用させていただきました。
- http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r10-105752.txt.bz2
2015年1月12日時点のWikipedia記事データベースのダンプ(英語)うち,約400語以上で構成される記事の中から,ランダムに1/10サンプリングした105,752記事のテキストをbzip2形式で圧縮したものです.このファイルはクリエイティブ・コモンズ 表示-継承 3.0 非移植のライセンスで配布されています.
サイズとしては140MBとなり、出現トークン数は重複ありで2300万、重複を取り除くと28万ほどとなりました。
リソースの状況は、Java Flight Recorderを使用して確認しています。
キャッシュなし
キャッシュ無しの場合の実行結果です。 メモリは最大で1.45GBまで使用されています。処理時間は27秒ほどでした。
キャッシュあり
キャッシュありの場合の実行結果です。 メモリは最大で792MBまで使用されています。処理時間は10秒ほどでした。
メモリ使用量で約半分、処理時間も半分以下になっています。処理時間が短縮されたのはGCにかかる時間が減ったためのようです。(使用するメモリ量も減ったので)
追記@2017-10-30
.map(token -> token.intern()) でも同じ効果になると思います。こちらであれば、JVM 全体でキャッシュしてくれます(システム全体の文字列量が多いと遅くなるかもしれません…)
— Yuji@いつだって眠い (@YujiSoftware) 2017年10月28日
と教えていただいたので、internを使って試してみたところ、同じような効果を得られました。
まとめ
同じトークンの出現頻度が多ければ、キャッシュすることによってメモリ使用量を抑える効果があることがわかりました。
同じトークンが少ない場合には、効果が薄い(逆にHashMap分余計にメモリを使うことになりかねない)ため、扱うデータの性質などによって、キャッシュするかどうか考えたほうがよさそうです。(小さなテキストでも試しましたが、ほとんど効果を得られませんでした)