フォルダー内の数千のテキストファイル(〜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ファイル)からのサンプルでは完全に正常に機能します。 5Kファイルを超える入力全体を実行した場合、かかる時間は線形増分に近いものではありませんでした。パフォーマンス分析を行うことを計画し、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()
。
私はこの点に同意しますが、ガベージコレクターは、ループの最後にメモリからロードされたコンテンツを自動的にクリアする必要があります。したがって、メモリは現在処理中のファイルのコンテンツのみを持つべきです。しかし、ここにはいくつかの落とし穴があります。誰かがこの問題に関する洞察を与えることができます。
これはreadlines()
の固有の動作ですか、またはpythonガベージコレクターの間違った解釈です。
また、メモリと時間の効率的な方法で同じことを行ういくつかの代替方法を提案します。 TIA。
短いバージョンは次のとおりです。 readlines()
を使用する効率的な方法は、使用しないことです。Ever。
私は
readlines()
に関するドキュメントノートを読みます。このreadlines()
はファイルの内容全体をメモリに読み込むため、一般にreadline()またはread()と比較してより多くのメモリを消費すると主張しています。
readlines()
のドキュメントは、ファイル全体をメモリに読み込み、解析することを明示的に保証します行、およびこれらの行からlist
ingsでいっぱいのstr
を構築します。
しかし、 read()
のドキュメントは同様に、ファイル全体をメモリに読み込み、str
ingを構築することを保証しているため、役に立ちません。
これは、より多くのメモリを使用することに加えて、すべてが読み取られるまで作業を実行できないことも意味します。最も単純な方法で読み取りと処理を交互に行う場合、少なくともいくつかのパイプライン処理(OSディスクキャッシュ、DMA、CPUパイプラインなどのおかげ)の恩恵を受けるため、次のバッチ中に1つのバッチで作業することになります。読んでいます。ただし、コンピューターにファイル全体を読み取らせ、ファイル全体を解析してからコードを実行するように強制すると、読み取りごとに重複する作業の1つの領域ではなく、ファイル全体に対して重複する作業の1つの領域のみが得られます。
これを回避するには、次の3つの方法があります。
readlines(sizehint)
、read(size)
、またはreadline()
の周りにループを作成します。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は、処理を高速化するためにNiceバッファーサイズを選択できます(および選択します)。
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の参照カウントを使用しています。つまり、コード内でfile_content
がリバウンドまたは消滅し、文字列の巨大なリストとその中のすべての文字列がフリーリストに解放されます。つまり、同じメモリを次のパスで再利用できます。
ただし、これらの割り当て、コピー、および割り当て解除はすべて無料ではありません。実行するよりも実行しないほうがはるかに高速です。
その上、同じ小さなメモリチャンクを何度も繰り返し使用するのではなく、文字列を大量のメモリに分散させると、キャッシュの動作が損なわれます。
さらに、メモリ使用量は一定(または、ファイルサイズの合計ではなく、最大ファイルのサイズで線形)である場合がありますが、malloc
sのラッシュは初めてそれを拡張します最も遅い処理の1つになります(これにより、パフォーマンスの比較が非常に難しくなります)。
すべてをまとめると、次のようにプログラムを記述します。
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 ...
ファイル全体ではなく、1行ずつ読み取ります。
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行ずつファイルオブジェクトを読み取ります。