簡體   English   中英

有沒有辦法繞過 Python list.append() 隨着列表的增長而在循環中逐漸變慢?

[英]Is there a way to circumvent Python list.append() becoming progressively slower in a loop as the list grows?

我有一個正在讀取的大文件,並將每隔幾行轉換為一個對象的實例。

由於我正在遍歷文件,因此我使用 list.append(instance) 將實例存儲到列表中,然后繼續循環。

這是一個大約 100MB 左右的文件,所以它不是太大,但隨着列表變大,循環逐漸減慢。 (我打印循環中每一圈的時間)。

這不是循環固有的〜當我在循環文件時打印每個新實例時,程序以恆定速度運行〜只有當我將它們附加到列表時它才會變慢。

我的朋友建議在 while 循環之前禁用垃圾收集並在之后啟用它並進行垃圾收集調用。

有沒有其他人觀察到 list.append 變慢的類似問題? 有沒有其他方法可以規避這種情況?


我將嘗試以下建議的以下兩件事。

(1)“預分配”內存~最好的方法是什么? (2) 嘗試使用雙端隊列

多個帖子(請參閱 Alex Martelli 的評論)建議內存碎片(他像我一樣擁有大量可用內存)〜但沒有明顯的性能修復。

要復制該現象,請運行答案中提供的測試代碼,並假設列表具有有用的數據。


gc.disable() 和 gc.enable() 有助於計時。 我還將仔細分析所有時間都花在了哪里。

您觀察到的性能不佳是由您正在使用的版本中的 Python 垃圾收集器中的錯誤引起的。 升級到 Python 2.7 或 3.1 或更高版本,以重新獲得 Python 中列表附加所期望的不折不扣的 0(1) 行為。

如果您無法升級,請在構建列表時禁用垃圾收集,並在完成后將其打開。

(您還可以調整垃圾收集器的觸發器或在您進行過程中選擇性地調用 collect,但我不會在此答案中探討這些選項,因為它們更復雜,我懷疑您的用例適合上述解決方案。)

背景:

請參閱: https ://bugs.python.org/issue4074 和https://docs.python.org/release/2.5.2/lib/module-gc.html

記者觀察到,將復雜對象(不是數字或字符串的對象)添加到列表會隨着列表長度的增加而線性減慢。

這種行為的原因是垃圾收集器正在檢查並重新檢查列表中的每個對象,以查看它們是否有資格進行垃圾收集。 此行為會導致將對象添加到列表的時間線性增加。 預計會在 py3k 中進行修復,因此它不應該適用於您正在使用的解釋器。

測試:

我進行了一個測試來證明這一點。 對於 1k 次迭代,我將 10k 個對象附加到一個列表中,並記錄每次迭代的運行時間。 整體運行時差異立即顯而易見。 在測試的內部循環期間禁用垃圾收集,我的系統上的運行時間為 18.6 秒。 為整個測試啟用垃圾回收后,運行時間為 899.4s。

這是測試:

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

由於這兩個圖在 y 分量上相差幾個數量級,因此它們在 y 軸上獨立地進行了線性縮放。


(來源: hypervolu.me


(來源: hypervolu.me

有趣的是,在關閉垃圾收集的情況下,我們只看到每 10k 附加的運行時出現小峰值,這表明 Python 的列表重新分配成本相對較低。 無論如何,它們比垃圾收集成本低許多數量級。

上面的圖的密度很難看出在垃圾收集器開啟的情況下,大多數區間實際上都有很好的性能; 只有當垃圾收集器循環時,我們才會遇到病態行為。 您可以在這個 10k 附加時間的直方圖中觀察到這一點。 大多數數據點每 10k 追加下降約 0.02 秒。


(來源: hypervolu.me

用於生成這些圖的原始數據可以在http://hypervolu.me/~erik/programming/python_lists/找到

沒有什么可以規避的:附加到列表是 O(1) 攤銷的。

列表(在 CPython 中)是一個數組,其長度至少與列表一樣長,最多是其兩倍。 如果數組未滿,則追加到列表就像分配數組成員之一一樣簡單 (O(1))。 每次數組滿時,它的大小都會自動加倍。 這意味着有時需要一個 O(n) 操作,但只需要每 n 個操作,並且隨着列表變得越來越大,它越來越少需要。 O(n) / n ==> O(1)。 (在其他實現中,名稱和細節可能會發生變化,但同時必須維護屬性。)

附加到列表已經可以擴展。

是否有可能當文件變大時,您無法將所有內容保存在內存中,並且您面臨操作系統分頁到磁盤的問題? 是否有可能是您的算法的不同部分不能很好地擴展?

很多這些答案只是瘋狂的猜測。 我最喜歡 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 內部結構,我不確定。 但我很確定列表數據結構不是罪魁禍首。

您可以嘗試 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支持

改為使用集合,然后在最后將其轉換為列表

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

我有同樣的問題,這解決了幾個訂單的時間問題。

我在使用 Numpy 數組時遇到了這個問題,創建如下:

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

隨着數組的增長,在循環中追加到這個數組所花費的時間越來越長,考慮到我有 14M 的追加,這是一個交易破壞者。

上面概述的垃圾收集器解決方案聽起來很有希望,但沒有奏效。

起作用的是創建具有預定義大小的數組,如下所示:

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

只要確保該限制大於您需要的數組即可。

然后,您可以直接設置數組中的每個元素:

theArray[i] = val_i

最后,如有必要,您可以刪除數組中未使用的部分

theArray = theArray[:i]

這對我的情況產生了巨大的影響。

嗨,我有一個非常相似的問題。 我創建了一個小型基准測試實用程序來尋找解決方案,我能夠完全解決問題,將速度提高數千倍。

解決方案:如果您使用子例程附加到列表,則該過程會隨着列表的增長而減慢。 如果您在內聯進行,則不會發生這種情況。

示例因此,在您的循環中,如果您這樣做,它將最終停止

list = sub_routine_append(list, new_element)

如果您內聯進行,則不會降級

list.append(new_element)

即使調用垃圾收集也沒有區別。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM