いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Pythonメモ-42 (mmapとreの組合せ) (パフォーマンス, メモリマップ, re.search, mmap.mmap)

概要

stackoverflow で以下のスレッドを見つけました。

stackoverflow.com

内容は

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 ドキュメント

www.post.japanpost.jp


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。