Существует ли способ обойти Python list.append () становящийся прогрессивно медленнее в цикле, когда список растет?

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

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

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

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

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

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


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

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

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

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


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

51
задан Deniz 19 March 2010 в 11:53
поделиться

4 ответа

Низкая производительность, которую вы наблюдаете, вызвана ошибкой в ​​сборщике мусора 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. По оси ординат отложены секунды в логарифмическом масштабе.


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

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


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


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

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

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


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

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

92
ответ дан 7 November 2019 в 09:59
поделиться

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

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

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

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

13
ответ дан 7 November 2019 в 09:59
поделиться

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

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

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
ответ дан 7 November 2019 в 09:59
поделиться

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

РЕДАКТИРОВАТЬ: Поддерживается http://www.python.org/doc/faq/general/#how-are-lists-implemented

1
ответ дан 7 November 2019 в 09:59
поделиться
Другие вопросы по тегам:

Похожие вопросы: