48

フォルダー内の数千のテキスト ファイル (サイズが ~400 KB の各ファイルに約 3000 行) を解析するのに問題があります。私はreadlinesを使ってそれらを読みました、

   for filename in os.listdir (input_dir) :
       if filename.endswith(".gz"):
          f = gzip.open(file, 'rb')
       else:
          f = open(file, 'rb')

       file_content = f.readlines()
       f.close()
   len_file = len(file_content)
   while i < len_file:
       line = file_content[i].split(delimiter) 
       ... my logic ...  
       i += 1  

これは、私の入力 (50,100 ファイル) からのサンプルに対して完全に正常に機能します。5,000 個を超えるファイルの入力全体を実行したところ、所要時間は直線的な増加にはほど遠いものでした。パフォーマンス分析を行う予定で、Cprofile 分析を行いました。入力が 7K ファイルに達すると、より多くのファイルにかかる時間が指数関数的に増加し、速度が低下します。

readlines の累積所要時間は次のとおりです。最初の -> 354 ファイル (入力からのサンプル) および 2 番目の -> 7473 ファイル (入力全体)

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 354    0.192    0.001    **0.192**    0.001 {method 'readlines' of 'file' objects}
 7473 1329.380    0.178  **1329.380**    0.178 {method 'readlines' of 'file' objects}

このため、私のコードにかかる時間は、入力が増加するにつれて線形にスケーリングされません。に関するいくつかのドキュメント ノートを読みましたが、これはファイル コンテンツ全体をメモリに読み込むため、またはに比べて一般的により多くのメモリを消費するとreadlines()主張していました。readlines()readline()read()

私はこの点に同意しますが、ガベージコレクターはループの最後にメモリからロードされたコンテンツを自動的にクリアする必要があるため、いつでもメモリには現在処理されているファイルのコンテンツのみが含まれている必要があります。しかし、ここで落とし穴があります。誰かがこの問題についていくつかの洞察を与えることができます.

これはPythonガベージコレクターの固有の動作ですか、readlines()それとも私の間違った解釈ですか。知ってよかった。

また、メモリと時間効率の良い方法で同じことを行ういくつかの代替方法を提案してください。ティア。

4

2 に答える 2

97

短いバージョンは次のとおりです。効率的な使用方法readlines()は、使用しないことです。これまで。


私はいくつかのドキュメント ノートを読みましたが、これはファイル コンテンツ全体をメモリに読み込むため、一般的に readline() や read() と比較してより多くのメモリを消費するとreadlines()主張していました。readlines()

のドキュメントは、ファイル全体をメモリに読み込み、それを行に解析し、それらの行から完全なingsを構築することを明示的に保証します。readlines() liststr

しかし、read()同様のドキュメントでは、ファイル全体をメモリに読み込み、ing を構築することが保証されているstrため、役に立ちません。


これは、より多くのメモリを使用することに加えて、すべてが読み取られるまで作業を行うことができないことも意味します。最も単純な方法でも読み取りと処理を交互に行うと、少なくとも一部のパイプライン (OS ディスク キャッシュ、DMA、CPU パイプラインなどのおかげ) の恩恵を受けるため、次のバッチで作業している間に 1 つのバッチで作業することになります。読まれています。しかし、コンピューターにファイル全体を強制的に読み取らせ、ファイル全体を解析してからコードを実行させると、読み取りごとに重複する作業の 1 つの領域ではなく、ファイル全体に対して重複する作業の 1 つの領域しか得られません。


これは、次の 3 つの方法で回避できます。

  1. readlines(sizehint)read(size)、またはの周りにループを記述しますreadline()
  2. これらのいずれも呼び出さずに、ファイルを遅延反復子として使用するだけです。
  3. mmapこれにより、最初に読み込むことなく巨大な文字列として扱うことができます。

たとえば、これはすべてをfoo一度に読み取る必要があります。

with open('foo') as f:
    lines = f.readlines()
    for line in lines:
        pass

ただし、これは一度に約 8K しか読み取れません。

with open('foo') as f:
    while True:
        lines = f.readlines(8192)
        if not lines:
            break
        for line in lines:
            pass

これは一度に 1 行しか読み取れませんが、Python では処理を高速化するために適切なバッファー サイズを選択することが許可されています (そして選択する予定です)。

with open('foo') as f:
    while True:
        line = f.readline()
        if not line:
            break
        pass

そして、これは前とまったく同じことを行います:

with open('foo') as f:
    for line in f:
        pass

その間:

しかし、ガベージコレクターは、ループの最後にメモリからロードされたコンテンツを自動的にクリアする必要があるため、いつでもメモリには現在処理されているファイルのコンテンツのみが含まれている必要がありますか?

Python は、ガベージ コレクションについてそのような保証を行いません。

CPython の実装では、たまたま GC に refcounting を使用しています。これは、コード内で、file_contentリバウンドまたは消失するとすぐに、文字列の巨大なリストとその中のすべての文字列がフリーリストに解放されることを意味します。つまり、同じことを意味します。メモリは次のパスで再利用できます。

ただし、これらすべての割り当て、コピー、および解放は無料ではありません。実行するよりも、実行しない方がはるかに高速です。

その上、メモリの同じ小さなチャンクを何度も再利用するのではなく、文字列を大量のメモリに分散させると、キャッシュの動作が損なわれます。

さらに、メモリ使用量は一定である可能性があります (または、ファイル サイズの合計ではなく、最大のファイルのサイズに比例します) malloc。あなたがすること(これはまた、パフォーマンスの比較を行うことをはるかに難しくします).


すべてをまとめると、次のようにプログラムを記述します。

for filename in os.listdir(input_dir):
    with open(filename, 'rb') as f:
        if filename.endswith(".gz"):
            f = gzip.open(fileobj=f)
        words = (line.split(delimiter) for line in f)
        ... my logic ...  

または多分:

for filename in os.listdir(input_dir):
    if filename.endswith(".gz"):
        f = gzip.open(filename, 'rb')
    else:
        f = open(filename, 'rb')
    with contextlib.closing(f):
        words = (line.split(delimiter) for line in f)
        ... my logic ...
于 2013-06-22T00:55:10.857 に答える
18

ファイル全体ではなく、行ごとに読み取ります。

for line in open(file_name, 'rb'):
    # process line here

withファイルを自動的に閉じるためのさらに良い使い方:

with open(file_name, 'rb') as f:
    for line in f:
        # process line here

上記は、イテレータを使用して一度に 1 行ずつファイル オブジェクトを読み取ります。

于 2013-06-22T00:49:34.673 に答える