Есть ли способ обходить Python list.append(), становясь все медленнее в цикле, когда список растет?

У меня есть большой файл, который я читаю, и преобразовываю все несколько строк в экземпляр объекта.

Так как я перебираю файл, я помещаю экземпляр в список, используя list.append(экземпляр), а затем продолжаю цикл.

Это файл размером около 100 МБ, поэтому он не слишком большой, но по мере увеличения списка цикл циклически замедляется. (Я печатаю время для каждого круга в цикле).

Это не является неотъемлемой частью цикла ~, когда я печатаю каждый новый экземпляр при прохождении цикла через файл, программа прогрессирует с постоянной скоростью ~ это только когда я добавляю их в список, который он замедляет.

Мой друг предложил отключить сбор мусора перед циклом while и включить его позже и сделать вызов коллекции мусора.

Кто-нибудь еще наблюдал аналогичную проблему, когда list.append стал медленнее? Есть ли другой способ обойти это?


Я попробую следующие две вещи, предложенные ниже.

(1) "предварительное выделение" памяти - какой лучший способ сделать это? (2) Попробуйте использовать deque

Несколько сообщений (см. комментарий Alex Martelli) предполагали фрагментацию памяти (у него много доступной памяти, как и я) ~, но никаких очевидных ошибок в производительности для этого нет.

Чтобы воспроизвести это явление, запустите тестовый код, приведенный ниже в ответах, и предположите, что списки имеют полезные данные.


gc.disable() и gc.enable() помогают с синхронизацией. Я также буду тщательно анализировать, где все время тратится.

Ответ 1

Низкая производительность, которую вы наблюдаете, вызвана ошибкой в сборщике мусора Python в версии, которую вы используете. Обновите до Python 2.7, или 3.1 или выше, чтобы восстановить амортизированное поведение 0 (1), ожидаемое при добавлении списка в Python.

Если вы не можете выполнить обновление, отключите сборку мусора при создании списка и включите его после завершения.

(Вы также можете настроить триггеры сборщика мусора или выборочно вызвать сборщик по мере продвижения, но я не рассматриваю эти варианты в этом ответе, потому что они более сложны, и я подозреваю, что ваш вариант использования подпадает под приведенное выше решение.)

Фон:

См.: https://bugs.python.org/issue4074, а также https://docs.python.org/release/2.5.2/lib/module-gc.html.

Репортер отмечает, что добавление сложных объектов (объектов, которые не являются числами или строками) в список линейно замедляется по мере увеличения длины списка.

Причина такого поведения заключается в том, что сборщик мусора проверяет и перепроверяет каждый объект в списке, чтобы определить, имеют ли они право на сборку мусора. Такое поведение вызывает линейное увеличение времени для добавления объектов в список. Ожидается, что исправление появится в py3k, поэтому оно не должно применяться к используемому вами интерпретатору.

Тестовое задание:

Я провел тест, чтобы продемонстрировать это. Для 1k итераций я добавляю 10k объектов в список и записываю время выполнения для каждой итерации. Общая разница во времени выполнения очевидна. Если во время внутреннего цикла теста сборка мусора отключена, время выполнения в моей системе составляет 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)

Поскольку эти два графика отличаются на несколько порядков величины по компоненте y, здесь они независимо с осью y линейно масштабируются.


(источник: hypervolu.me)


(источник: hypervolu.me)

Интересно, что при отключенной сборке мусора мы видим только небольшие всплески времени выполнения на 10 тыс. Добавлений, что говорит о том, что затраты на перераспределение списка Python относительно невелики. В любом случае они на много порядков ниже затрат на сборку мусора.

Плотность вышеприведенных графиков затрудняет понимание того, что при включенном сборщике мусора большинство интервалов действительно имеют хорошую производительность; только когда циклы сборщика мусора встречаются с патологическим поведением. Вы можете наблюдать это на гистограмме времени добавления 10 тыс. Большинство точек данных падает около 0,02 с на 10 тыс. Добавлений.


(источник: hypervolu.me)

Необработанные данные, использованные для создания этих графиков, можно найти по адресу http://hypervolu.me/~erik/programming/python_lists/

Ответ 2

Нет ничего, чтобы обойти: добавление к списку O (1) амортизировано.

Список (в CPython) представляет собой массив, по крайней мере, до тех пор, как список и в два раза больше. Если массив не заполнен, добавление к списку так же просто, как назначение одного из элементов массива (O (1)). Каждый раз, когда массив заполнен, он автоматически удваивается по размеру. Это означает, что иногда требуется операция O (n), но она требуется только для каждого n операций, и это становится все более и более редко, поскольку список становится большим. O (n)/n == > O (1). (В других реализациях имена и детали могут потенциально меняться, но сохраняются те же свойства времени.)

Добавление к списку уже масштабируется.

Возможно ли, что когда файл станет большим, вы не сможете хранить все в памяти, и вы сталкиваетесь с проблемами с подкачкой ОС на диск? Возможно ли, что это другая часть вашего алгоритма, которая недостаточно масштабируется?

Ответ 3

Многие из этих ответов - просто дикие догадки. Мне нравится Майк Грэм, потому что он прав насчет того, как списки реализованы. Но я написал некоторый код, чтобы воспроизвести ваше выражение и изучить его дальше. Вот некоторые выводы.

Вот что я начал с.

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, которые могли бы объяснить это, я не уверен. Но я уверен, что структура данных списка не является виновником.

Ответ 4

Можете ли вы попробовать http://docs.python.org/release/2.5.2/lib/deque-objects.html выделить ожидаемое количество необходимых элементов в вашем списке?? Я бы поспорил, что список - это непрерывное хранилище, которое необходимо перераспределить и скопировать каждые несколько итераций. (аналогично некоторым популярным реализациям std::vector в С++)

EDIT: резервное копирование http://www.python.org/doc/faq/general/#how-are-lists-implemented

Ответ 5

Я столкнулся с этой проблемой при использовании массивов Numpy, созданных следующим образом:

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

Добавление к этому массиву в цикле продолжалось дольше по мере роста массива, что было разрывом сделки, учитывая, что я добавил 14M, чтобы сделать.

Решение сборщика мусора, изложенное выше, показало себя многообещающим, но не сработало.

Что работала над созданием массива с предопределенным размером следующим образом:

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

Просто убедитесь, что limit больше, чем требуемый массив.

Вы можете сразу установить каждый элемент в массиве:

theArray[i] = val_i

И в конце, если необходимо, вы можете удалить неиспользуемую часть массива

theArray = theArray[:i]

Это привело к большой разнице в моем случае.

Ответ 6

Используйте набор, а затем преобразуйте его в список в конце

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

У меня была та же проблема, и это позволило решить проблему времени несколькими порядками.