Python: невозможно повторить тест на использование памяти - PullRequest
0 голосов
/ 25 июня 2018

Я пытался повторить тест использования памяти здесь .

По сути, пост утверждает, что с учетом следующего фрагмента кода:

import copy
import memory_profiler

@profile
def function():
    x = list(range(1000000))  # allocate a big list
    y = copy.deepcopy(x)
    del x
    return y

if __name__ == "__main__":
    function()

Invoking

python -m memory_profiler memory-profile-me.py

распечатывает на 64-битном компьютере

Filename: memory-profile-me.py

Line #    Mem usage    Increment   Line Contents
================================================
 4                             @profile
 5      9.11 MB      0.00 MB   def function():
 6     40.05 MB     30.94 MB       x = list(range(1000000)) # allocate a big list
 7     89.73 MB     49.68 MB       y = copy.deepcopy(x)
 8     82.10 MB     -7.63 MB       del x
 9     82.10 MB      0.00 MB       return y

Я скопировал и вставил тот же код, но мой профилировщик выдает

Line #    Mem usage    Increment   Line Contents
================================================
 3   44.711 MiB   44.711 MiB   @profile
 4                             def function():
 5   83.309 MiB   38.598 MiB       x = list(range(1000000))  # allocate a big list
 6   90.793 MiB    7.484 MiB       y = copy.deepcopy(x)
 7   90.793 MiB    0.000 MiB       del x
 8   90.793 MiB    0.000 MiB       return y

Это сообщение может быть устаревшим -- либо пакет профилировщика, либо python могли измениться.В любом случае, мои вопросы в Python 3.6.x

(1) Должен ли copy.deepcopy(x) (как определено в коде выше) потреблять нетривиальный объем памяти?

(2)Почему я не могу выполнить репликацию?

(3) Если я повторю x = list(range(1000000)) после del x, увеличится ли объем памяти на ту же величину, которую я впервые назначил x = list(range(1000000)) (как в строке 5 моего кода)

1 Ответ

0 голосов
/ 25 июня 2018

copy.deepcopy() рекурсивно копирует только изменяемый объект , неизменяемые объекты, такие как целые числа или строки, не копируются.Копируемый список состоит из неизменяемых целых чисел, поэтому копия y в конечном итоге обменивается ссылками на те же самые целочисленные значения:

>>> import copy
>>> x = list(range(1000000))
>>> y = copy.deepcopy(x)
>>> x[-1] is y[-1]
True
>>> all(xv is yv for xv, yv in zip(x, y))
True

Таким образом, копии требуется только создать новый объект списка с 1 миллионом ссылок,объект, который занимает чуть более 8 МБ памяти в моей сборке Python 3.6 в Mac OS X 10.13 (64-разрядная ОС):

>>> import sys
>>> sys.getsizeof(y)
8697464
>>> sys.getsizeof(y) / 2 ** 20   # Mb
8.294548034667969

Пустой объект list занимает 64 байта, каждая ссылка занимает8 байтов:

>>> sys.getsizeof([])
64
>>> sys.getsizeof([None])
72

Объекты списка Python перераспределяют пространство для роста, преобразование объекта range() в список приводит к тому, что он занимает немного больше места для дополнительного роста, чем при использовании deepcopy, поэтому x немного больше по-прежнему, в нем есть место для дополнительных 125 тыс. объектов, прежде чем придется снова изменять его размер:

>>> sys.getsizeof(x)
9000112
>>> sys.getsizeof(x) / 2 ** 20
8.583175659179688
>>> ((sys.getsizeof(x) - 64) // 8) - 10**6
125006

, в то время как копия имеет только дополнительное пространство для хранения в течение приблизительно 87 КБ:

>>> ((sys.getsizeof(y) - 64) // 8) - 10**6
87175

На Python 3.6 я также не могу воспроизвести утверждения статьи, отчасти потому, что Python видел много улучшений в управлении памятью, а отчасти потому, что статья ошибочна на сервереal points.

Поведение copy.deepcopy() в отношении списков и целых чисел никогда не изменилось в длинной истории copy.deepcopy() (см. первую версию модуля , добавленов 1995 ), и интерпретация данных памяти неверна, даже на Python 2.7.

В частности, я могу воспроизвести результаты с использованием Python 2.7 Это то, что я вижу намоя машина:

$ python -V
Python 2.7.15
$ python -m memory_profiler memtest.py
Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   28.406 MiB   28.406 MiB   @profile
     5                             def function():
     6   67.121 MiB   38.715 MiB       x = list(range(1000000))  # allocate a big list
     7  159.918 MiB   92.797 MiB       y = copy.deepcopy(x)
     8  159.918 MiB    0.000 MiB       del x
     9  159.918 MiB    0.000 MiB       return y

Происходит то, что система управления памятью Python выделяет новый кусок памяти для дополнительного расширения.Дело не в том, что новый объект списка y занимает почти 93 МБ памяти, это просто дополнительная память, выделенная ОС для процесса Python, когда этот процесс запросил немного памяти для кучи объектов.Сам объект списка имеет размер lot меньше.

Модуль Python 3 tracemalloc намного точнее о том, что на самом деле происходит:

python3 -m memory_profiler --backend tracemalloc memtest.py
Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4    0.001 MiB    0.001 MiB   @profile
     5                             def function():
     6   35.280 MiB   35.279 MiB       x = list(range(1000000))  # allocate a big list
     7   35.281 MiB    0.001 MiB       y = copy.deepcopy(x)
     8   26.698 MiB   -8.583 MiB       del x
     9   26.698 MiB    0.000 MiB       return y

Менеджер памяти Python 3.x и реализация списка умнее, чем в 2.7;очевидно, новый объект списка смог уместиться в уже имеющуюся память, предварительно выделенную при создании x.

. Мы можем протестировать поведение Python 2.7 с помощью созданного вручную Python 2.7.12бинарный файл tracemalloc и небольшой патч до memory_profile.py.Теперь мы получаем еще более обнадеживающие результаты и в Python 2.7:

Filename: memtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4    0.099 MiB    0.099 MiB   @profile
     5                             def function():
     6   31.734 MiB   31.635 MiB       x = list(range(1000000))  # allocate a big list
     7   31.726 MiB   -0.008 MiB       y = copy.deepcopy(x)
     8   23.143 MiB   -8.583 MiB       del x
     9   23.141 MiB   -0.002 MiB       return y

Я отмечаю, что автор также был сбит с толку:

copy.deepcopy копирует оба списка, что снова распределяет~ 50 МБ ( Я не уверен, откуда дополнительные 10 МБ - 31 МБ = 19 МБ )

(выделено жирным шрифтом).

Ошибка здесь в том, что предполагается, что все изменения памяти в размере процесса Python можно напрямую отнести к конкретным объектам, но реальность гораздо сложнее, так как менеджер памяти может добавлять ( и удалять! ) память'arenas', блоки памяти, зарезервированные для кучи, по мере необходимости, и будут делать это в больших блоках, если это имеет смысл.Процесс здесь сложный, так как он зависит от взаимодействий между менеджером Python и ОС malloc, подробности реализации .Автор нашел более старую статью о модели Python, которую они неправильно поняли как актуальную, автор этой статьи уже пытался указать на это ;с Python 2.5 утверждение о том, что Python не освобождает память, больше не соответствует действительности.

Что беспокоит, так это то, что те же недоразумения побуждают автора рекомендовать не использовать pickle, но на самом деле модуль, даже в Python 2, никогда не добавляет больше, чем небольшая память для отслеживания рекурсивных структур.Смотрите эту суть моей методики тестирования ;использование cPickle в Python 2.7 добавляет разовое увеличение на 46 МБ (удвоение вызова create_file() не приводит к дальнейшему увеличению памяти).В Python 3 изменения памяти вообще исчезли.

Я открою диалог с командой Theano по поводу поста, статья ошибочна, сбивает с толку, и Python 2.7 скоро все равно будет полностью устаревшим, поэтомуони действительно должны сосредоточиться на модели памяти Python 3. (*)

Когда вы создаете новый список из range(), а не копию, вы увидите такое же увеличение памяти, что и при создании x в первый раз, потому что вы создали бы новый набор целочисленных объектов в дополнение к новому объекту списка.Помимо определенного набора маленьких целых чисел , Python не кэширует и не использует целочисленные значения для range() операций.


(*) addendum : Я открыл выпуск # 6619 с проектом Thano.Проект согласился с моей оценкой и удалил страницу из своей документации , хотя они еще не обновили опубликованную версию.

...