Функция Python замедляется при наличии большого списка

#python #performance #memory #allocation

#python #Производительность #память #распределение

Вопрос:

Я тестировал скорость нескольких различных способов выполнения сложных итераций над некоторыми моими данными и обнаружил кое-что странное. Похоже, что наличие большого списка, локального для какой-либо функции, значительно замедляет эту функцию, даже если она не касается этого списка. Например, создание 2 независимых списков с помощью 2 экземпляров одной и той же функции генератора во второй раз происходит примерно в 2,5 раза медленнее. Если первый список удаляется до создания второго, оба итератора выполняются с одинаковой скоростью.

 def f():  
    l1, l2 = [], []  
    for c1, c2 in generatorFxn():  
        l1.append((c1, c2))  
    # destroying l1 here fixes the problem 
    for c3, c4 in generatorFxn():  
        l2.append((c3, c4))
  

В конечном итоге каждый список содержит около 3,1 миллиона элементов, но я видел тот же эффект и с меньшими списками. Выполнение первого for цикла занимает около 4,5 секунд, второго — 10,5. Если я вставляю l1= [] или l1= len(l1) в позицию комментария, оба for цикла занимают 4,5 секунды.

Почему скорость выделения локальной памяти в функции имеет какое-либо отношение к текущему размеру переменных этой функции?

РЕДАКТИРОВАТЬ: Отключение сборщика мусора исправляет все, так что, должно быть, из-за того, что он работает постоянно. Дело закрыто!

Ответ №1:

Когда вы создаете так много новых объектов (3 миллиона кортежей), сборщик мусора увязает. Если вы отключите сборку мусора с помощью gc.disable(), проблема исчезнет (и программа загрузится в 4 раза быстрее).

Комментарии:

1. Вы правы, сэр, его отключение сокращает время с 4,5 до 1,3 секунды и почти устраняет различия (второй по-прежнему немного медленнее, но уже ненамного). Почему сборщик мусора работает так медленно даже после создания большого списка, который просто существует, а не изменяется? Разве это не должно быть сделано только после того, как функция вернется?

2. Кроме того, почему замедление исчезает, если l это переменная класса, а не локальная переменная?

3. Вот мое лучшее предположение: сборщик мусора периодически запускается для сбора старых объектов. Она определяет, когда запускать, сравнивая количество выделенных и освобожденных объектов с момента последней коллекции. Если выделено-освобождено > пороговое значение, сборщик запускается (и пороговое значение = 700 по умолчанию). Поскольку вы создаете (по крайней мере) 1 новый объект за итерацию, сборщик запускается 3e6 / 700 = 4285 раз. Это фактически замедляет обе итерации, но вторая итерация медленнее, поскольку есть больше объектов для проверки сборщиком.

4. Я добавлю два простых предложения, чтобы избежать этой проблемы: 1) начните с 2 списков и добавляйте к ним значения, а не создавайте 3 миллиона кортежей. 2) Увеличьте порог сбора мусора (по крайней мере временно) во время генерации списка

5. Спасибо, оба предложения имеют большой смысл, и я не забуду их использовать. Кроме того, при повторном тестировании сегодня установка l в качестве переменной класса, похоже, больше не устраняет проблему. Должно быть, вчера я тестировал что-то не так. Спасибо за помощь!

Ответ №2:

Невозможно сказать без более детального инструментария.

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

Комментарии:

1. Каждый элемент в списках представляет собой кортеж из 2 строк, каждая длиной в 3 символа. И я видел тот же эффект с меньшими списками; при размере списка 770 000 каждый время составляет 0,57 и 0,96 секунды. При 188 000 они составляют 0,11 и 0,15 секунды. Я совершенно уверен, что это не порог памяти, который я пересекаю, поскольку я использую 64-разрядную систему с 4 ГБ оперативной памяти.

Ответ №3:

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

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

Комментарии:

1. 1 для придания большого смысла и напоминания мне, что выделение памяти (обычно) не занимает 1 большой кусок. Согласно Activity Monitor, оба списка вместе занимали около 280 мб свободного места, так что это изрядный кусок.

Ответ №4:

Память, используемая данными, локальными для функции, не будет собираться мусором, пока функция не вернется. Если у вас нет необходимости выполнять нарезку, использование списков для больших наборов данных не является отличной идеей.

Из вашего примера не совсем ясно, какова цель создания этих списков. Возможно, вы захотите рассмотреть возможность использования генераторов вместо списков, особенно если списки будут просто повторяться. Если вам нужно выполнить нарезку возвращаемых данных, приведите генераторы к спискам в это время.

Комментарии:

1. Создание этих списков изначально было сделано просто для проверки скорости различных функций генератора, но мне придется создавать подобные списки, как только я найду подходящий алгоритм. Вопрос не в том, чтобы найти лучший способ хранения данных, а скорее в том, почему происходит такое резкое снижение производительности. Для меня это не имеет смысла, и я хотел бы помнить ответ при программировании в будущем. И я знаю, что сборка мусора не будет происходить в середине функции, поэтому повторное назначение l не должно устранять проблему (и все же это происходит).