概要
stackoverflow で以下のスレッドを見つけました。
内容は
linuxのgrepコマンドと比べて、pythonで組んでみた俺のgrepコード超遅いんだけどなんとかなんないかな?
って事なんですが、そこで回答された方が提示した案が以下の点。
メモリコスト上がるけど、一行一行見ずに
f.read()
で一気に見るmmap使う
でした。結果として、mmap使ったものがダントツで速いという結果に。
まあ、C言語で、かつ、めっちゃアルゴリズムも最適化されているgrepコマンドにはそもそも勝てないのですが
それでも、まあまあな結果になってました。
てことで、面白そうなので自分でもやってみようと思いました。
サンプル
以下の2ファイルで試してみました。
stackoverflow で書いてあった
/usr/share/dict/
のlinux.words
ファイル (479,624行) (約4.7MB)郵政省からダウンロードできる
KEN_ALL.csv
ファイル (124,162行) (約11.7MB)
これをそれぞれ
単純に一行ずつ
re.search
したパターンf.read()
で一気に読み込んでre.search
したパターンmmap使って
re.search
したパターン
で10回試行してどれくらい時間かかるのか試してみました。
# coding: utf-8 """ mmap モジュールについてのサンプルです。 通常の file-like オブジェクトを使ってのファイル処理より mmapオブジェクトの方が効率がいい場合があることを確認するサンプルです。 """ import itertools import mmap import re from timeit import timeit from typing import Union, Match from trypython.common.commoncls import SampleBase # noinspection SpellCheckingInspection class Sample(SampleBase): LINUX_WORDS = 'linux.words' KEN_ALL = 'KEN_ALL_UTF8.csv' LINUX_WORDS_PATTERN = 'zymurgies' KEN_ALL_PATTERN = '鳩間' def __init__(self): self._files = [ self.LINUX_WORDS, self.KEN_ALL ] self._methods = [ self.grep1, self.grep2, self.grep3 ] self._pattern_factory = { self.LINUX_WORDS: { self.grep1: lambda: self.LINUX_WORDS_PATTERN, self.grep2: lambda: self.LINUX_WORDS_PATTERN, self.grep3: lambda: self.LINUX_WORDS_PATTERN.encode('utf-8') }, self.KEN_ALL: { self.grep1: lambda: self.KEN_ALL_PATTERN, self.grep2: lambda: self.KEN_ALL_PATTERN, self.grep3: lambda: self.KEN_ALL_PATTERN.encode('utf-8') } } def exec(self): for f, m in itertools.product(self._files, self._methods): p = self._pattern_factory[f][m] r = timeit('m(p(), f)', number=10, globals=locals()) print(f'[file={f:20}|{m.__name__}]\t{r:0.3f}') @staticmethod def grep1(pattern: str, file_path: str) -> Union[Match, None]: with open(file_path, mode='r', encoding='utf-8') as f: for l in f: m = re.search(pattern, l) if m: return m return None @staticmethod def grep2(pattern: str, file_path: str) -> Union[Match, None]: with open(file_path, mode='r', encoding='utf-8') as f: return re.search(pattern, f.read()) # noinspection PyTypeChecker @staticmethod def grep3(pattern: bytes, file_path: str) -> Union[Match, None]: with open(file_path, mode='r', encoding='utf-8') as f: mm = mmap.mmap(f.fileno(), length=0, access=mmap.ACCESS_READ) return re.search(pattern, mm) def go(): obj = Sample() obj.exec() if __name__ == '__main__': go()
私の環境では以下のようになりました。
timeitのnumber=10の場合
[file=linux.words |grep1] 16.882 [file=linux.words |grep2] 0.120 [file=linux.words |grep3] 0.041 [file=KEN_ALL_UTF8.csv |grep1] 5.477 [file=KEN_ALL_UTF8.csv |grep2] 1.014 [file=KEN_ALL_UTF8.csv |grep3] 0.138
timeitのnumber=1の場合
[file=linux.words |grep1] 1.870 [file=linux.words |grep2] 0.014 [file=linux.words |grep3] 0.005 [file=KEN_ALL_UTF8.csv |grep1] 0.569 [file=KEN_ALL_UTF8.csv |grep2] 0.114 [file=KEN_ALL_UTF8.csv |grep3] 0.014
たしかにmmapの方が速いですね。
勉強になりました。
参考情報
27.5. timeit — 小さなコード断片の実行時間計測 — Python 3.6.3 ドキュメント
18.9. mmap — メモリマップファイル — Python 3.6.3 ドキュメント
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場