web-dev-qa-db-ja.com

Python list.append()を回避する方法はありますか?リストが大きくなるにつれてループ内で徐々に遅くなりますか?

読み込み中の大きなファイルがあり、数行ごとにオブジェクトのインスタンスに変換します。

ファイルをループしているので、list.append(instance)を使用してインスタンスをリストに隠し、ループを続けます。

これは約100MBのファイルなので、大きすぎませんが、リストが大きくなるにつれて、ループは徐々に遅くなります。 (ループ内の各ラップの時間を出力します)。

これはループに固有のものではありません〜ファイルをループするときにすべての新しいインスタンスを印刷すると、プログラムは一定の速度で進行します〜リストに追加したときだけ遅くなります。

私の友人は、whileループの前にガベージコレクションを無効にし、後で有効にして、ガベージコレクションの呼び出しを行うことを提案しました。

他の誰かがlist.appendの同様の問題を観察しましたか?これを回避する他の方法はありますか?


以下に示す2つのことを試してみます。

(1)メモリの「事前割り当て」〜これを行う最良の方法は何ですか? (2)dequeを使用してみてください

複数の投稿(Alex Martelliのコメントを参照)はメモリの断片化を示唆しました(彼は私と同じように大量のメモリを使用できます)〜しかし、これに対するパフォーマンスの明らかな修正はありません。

現象を再現するには、以下の回答で提供されるテストコードを実行し、リストに有用なデータがあると仮定してください。


gc.disable()およびgc.enable()はタイミングを支援します。また、すべての時間が費やされている場所を慎重に分析します。

51
Deniz

パフォーマンスの低下は、使用しているバージョンのPythonガベージコレクターのバグが原因です。アップグレードto Python 2.7、または3.1以上を使用して、Pythonでリストを追加する場合に期待されるamoritized 0(1)の動作を回復します。

アップグレードできない場合は、リストを作成するときにガベージコレクションを無効にし、終了後にオンにします。

(ガベージコレクターのトリガーを微調整したり、進行に応じて選択的にcollectを呼び出したりすることもできますが、これらのオプションはより複雑であり、ユースケースが上記のソリューションに適していると思われるため、この回答ではこれらのオプションを検討しません)

バックグラウンド:

参照: https://bugs.python.org/issue4074 および https://docs.python.org/release/2.5.2/lib/module-gc.html

レポーターは、リストの長さが大きくなるにつれて、リストに複雑なオブジェクト(数字でも文字列でもないオブジェクト)を追加すると、直線的に遅くなることを観察します。

この動作の理由は、ガベージコレクタがリスト内のすべてのオブジェクトをチェックおよび再チェックして、それらがガベージコレクションに適格であるかどうかを確認するためです。この動作により、リストにオブジェクトを追加する時間が直線的に増加します。修正はpy3kで行われる予定であるため、使用しているインタープリターには適用しないでください。

テスト:

これを実証するテストを実行しました。 1k回の繰り返しの場合、リストに1万個のオブジェクトを追加し、各繰り返しのランタイムを記録します。全体的なランタイムの違いはすぐに明らかになります。テストの内部ループ中にガベージコレクションが無効になっていると、システムのランタイムは18.6秒になります。テスト全体でガベージコレクションを有効にすると、ランタイムは899.4秒になります。

これはテストです:

import time
import gc

class A:
    def __init__(self):
        self.x = 1
        self.y = 2
        self.why = 'no reason'

def time_to_append(size, append_list, item_gen):
    t0 = time.time()
    for i in xrange(0, size):
        append_list.append(item_gen())
    return time.time() - t0

def test():
    x = []
    count = 10000
    for i in xrange(0,1000):
        print len(x), time_to_append(count, x, lambda: A())

def test_nogc():
    x = []
    count = 10000
    for i in xrange(0,1000):
        gc.disable()
        print len(x), time_to_append(count, x, lambda: A())
        gc.enable()

完全なソース: https://hypervolu.me/~erik/programming/python_lists/listtest.py.txt

グラフィカルな結果:赤はgcがオンで、青はgcがオフです。 y軸は、対数的にスケーリングされた秒です。


(ソース: hypervolu.me

2つのプロットはy成分が数桁異なるため、ここではy軸が線形にスケーリングされて独立しています。


(ソース: hypervolu.me


(ソース: hypervolu.me

興味深いことに、ガベージコレクションがオフの場合、追加された10kあたりの実行時間はわずかに増加します。これは、Pythonのリスト再割り当てコストが比較的低いことを示しています。いずれにせよ、それらはガベージコレクションのコストよりも桁違いに低くなっています。

上記のプロットの密度により、ガベージコレクターをオンにすると、ほとんどの間隔で実際に良好なパフォーマンスが得られることがわかりにくくなります。ガベージコレクターが循環するときに初めて病理学的な動作が発生します。これは、10kの追加時間のこのヒストグラムで確認できます。ほとんどのデータポイントは、10kの追加ごとに約0.02秒になります。


(ソース: hypervolu.me

これらのプロットの作成に使用される生データは、 http://hypervolu.me/~erik/programming/python_lists/ にあります。

92
Erik Garrison

回避するものは何もありません:リストに追加するO(1)償却済み

リスト(CPython)は、少なくともリストと同じ長さで、最大2倍の配列です。配列がいっぱいでない場合、リストへの追加は、配列メンバーの1つ(O(1))を割り当てるのと同じくらい簡単です。配列がいっぱいになるたびに、サイズは自動的に2倍になります。これは、場合によってO(n)操作が必要ですが、n操作ごとにのみ必要です、リストが大きくなるにつれて、それが必要になることはめったにありません。O(n)/n ==> O(1)。時間プロパティは維持されます。)

リストへの追加はすでにスケーリングされています。

ファイルが大きくなると、すべてをメモリに保持できず、OSがディスクにページングする問題に直面する可能性はありますか?アルゴリズムの別の部分がうまくスケールしない可能性はありますか?

14
Mike Graham

これらの答えの多くは、単なる推測です。リストの実装方法については正しいので、Mike Grahamは最高です。しかし、私はあなたの主張を再現し、さらに調査するためのコードをいくつか書きました。ここにいくつかの調査結果があります。

これが私が始めたものです。

_import time
x = []
for i in range(100):
    start = time.clock()
    for j in range(100000):
        x.append([])
    end = time.clock()
    print end - start
_

リストxに空のリストを追加しています。私は、100,000回の追加ごとに100回、期間を印刷します。あなたが主張したように遅くなります。 (最初の反復で0.03秒、最後の反復で0.84秒...かなり違います。)

明らかに、リストをインスタンス化するが、xに追加しない場合、リストはずっと速く実行され、時間の経過とともにスケールアップしません。

ただし、x.append([])x.append('hello world')に変更すると、速度はまったく向上しません。同じオブジェクトが100 * 100,000回リストに追加されています。

私がこれを作るもの:

  • 速度の低下は、リストのサイズとは関係ありません。生きているPythonオブジェクトの数に関係しています。
  • リストにアイテムをまったく追加しないと、それらはただちにガベージコレクションされ、Pythonによって管理されなくなります。
  • 同じアイテムを繰り返し追加する場合、ライブPythonオブジェクトの数は増えません。しかし、リストは時々自分自身のサイズを変更する必要があります。しかし、これはそうではありませんパフォーマンスの問題の原因。
  • 新しく作成した多数のオブジェクトを作成してリストに追加しているため、それらはライブのままであり、ガベージコレクションされません。スローダウンはおそらくこれと関係があります。

これを説明できるPythonの内部についてはわかりません。しかし、リストのデータ構造が原因ではないと確信しています。

6
FogleBird

次のように作成されたNumpy配列の使用中にこの問題が発生しました。

import numpy
theArray = array([],dtype='int32')

ループ内でのこの配列への追加は、配列が大きくなるにつれて次第に長くかかりました。

上記のガベージコレクターソリューションは有望に思えましたが、機能しませんでした。

うまくいったのは、次のように事前定義されたサイズで配列を作成することでした。

theArray = array(arange(limit),dtype='int32')

limitが必要な配列よりも大きいことを確認してください。

その後、配列内の各要素を直接設定できます。

theArray[i] = val_i

最後に、必要に応じて、配列の未使用部分を削除できます

theArray = theArray[:i]

私の場合、これは大きな違いをもたらしました。

1
Nathan Labenz

試してもらえますか http://docs.python.org/release/2.5.2/lib/deque-objects.html リストに必要な要素の予想数を割り当てますか? ?リストは連続したストレージであり、数回の反復ごとに再割り当ておよびコピーする必要があると思います。 (C++のstd :: vectorのいくつかの一般的な実装と同様)

編集: http://www.python.org/doc/faq/general/#how-are-lists-implemented によりバックアップ

1

代わりにセットを使用し、最後にリストに変換します

my_set=set()
with open(in_file) as f:
    # do your thing
    my_set.add(instance)


my_list=list(my_set)
my_list.sort() # if you want it sorted

私は同じ問題を抱えており、これはいくつかの注文で時間の問題を解決しました。

1
Kahiga