Уже пришло уточнение: «не техдиректор, а разработчик»:
Видимо дальше как в анекдоте: «не 500к биткоинов, а 500к рублей. и не российских, а белорусских. и не намайнил, а в тубмочке нашёл»
Похоже, я был зомбирован статьями и комментариями в интернете, которые уже успели устареть. Почитал исходный код и кое-чего уяснил. Я попробую проиллюстрировать свои открытия на примерах, но сначала немного теории…
Объекты Int (как и некоторые другие встроенные типы) поддерживают собственный аллокатор памяти. При необходимости память запрашивается вызовом malloc (в коде используется макрос PyMem_MALLOC, но это просто обертка вокруг malloc; не путать с PyMem_Malloc) блоками по ~1kb.
Есть два связанных списка: block_list — список всех выделенных блоков памяти и free_list — список всех свободных ячеек в этих блоках. Когда создается новый Int, то первым делом (еще есть кэширование значений от -5 до 256, но не будем об этом...) проверяется указатель free_list. Если он не равен null, то Int записывается в свободную ячейку, а free_list укорачивается на один элемент. Если null, то значит, свободных ячеек больше нет. В этом случае вызывается функция fill_free_list, которая запрашивает у malloc новый блок и добавляет его ячейки в список free_list.
Также есть функция PyInt_ClearFreeList, которая отыскивает среди block_list-ов такие, которые полностью состоят из свободных ячеек, вырезает их из списка и вызывает на них malloc_free (опять через обертку PyMem_FREE). Эта функция (в первоначальном название PyInt_CompactFreeList) была добавлена в версии python 2.6 alpha 1 (см. bugs.python.org/issue1953). Следующие комментарий в начале модуля intobject.c явным образом отрицает ее существование, потому что он был написан на 6 лет раньше и с тех пор не обновлялся. Не верьте ему :).
hg blame intobject.c
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: /* Integers are quite normal objects, to make object handling uniform.
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: (Using odd pointers to represent integers would save much space
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: but require extra checks for this special case throughout the code.)
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: Since a typical Python program spends much of its time allocating
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: and deallocating integers, these operations should be very fast.
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: Therefore we use a dedicated allocation scheme with a much lower
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: overhead (in space and time) than straight malloc(): a simple
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: dedicated free list, filled when necessary with memory from malloc().
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: block_list is a singly-linked list of all PyIntBlocks ever allocated,
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: linked via their next members. PyIntBlocks are never returned to the
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: system before shutdown (PyInt_Fini).
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000:
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: free_list is a singly-linked list of available PyIntObjects, linked
tim 01478a132908 Sun Apr 28 16:57:34 2002 +0000: via abuse of their ob_type members.
guido a6934380c6e7 Thu Dec 20 15:06:42 1990 +0000: */
Сама функция PyInt_ClearFreeList регулярно дергается сборщиком мусора. Вот так выглядит ее вызов в версии 2.7.5:
Modules/gcmodule.c
/* This is the main function. Read this to understand how the
* collection process works. */
static Py_ssize_t
collect(int generation)
{
…
здесь много кода
...
/* Clear free list only during the collection of the highest
* generation */
if (generation == NUM_GENERATIONS-1) {
clear_freelists();
}
/* Clear all free lists
* All free lists are cleared during the collection of the highest generation.
* Allocated items in the free list may keep a pymalloc arena occupied.
* Clearing the free lists may give back memory to the OS earlier.
*/
static void
clear_freelists(void)
{
(void)PyMethod_ClearFreeList();
(void)PyFrame_ClearFreeList();
(void)PyCFunction_ClearFreeList();
(void)PyTuple_ClearFreeList();
#ifdef Py_USING_UNICODE
(void)PyUnicode_ClearFreeList();
#endif
(void)PyInt_ClearFreeList();
(void)PyFloat_ClearFreeList();
}
Итак, обещанные примеры.
Для наблюдения за расходом памяти я использовал memory_profiler в связке с psutil. Чтобы освободить память я напрямую дергаю PyInt_ClearFreeList (через ctypes.pythonapi) и, на всякий случай, gc.collect(2), чтобы доказать, что ничего нового не произойдет.
Пример 1. Здесь все хорошо.
Filename: tests/test_good.py
Line # Mem usage Increment Line Contents
================================================
6 @profile
7 5.785 MB 0.000 MB def func():
8 21.117 MB 15.332 MB a = range(10**6)
9 17.301 MB -3.816 MB del a
10 5.895 MB -11.406 MB ctypes.pythonapi.PyInt_ClearFreeList()
11 5.895 MB 0.000 MB gc.collect(2)
Мы забрали память у ОС, а потом его вернули. Вот бы так было всегда…
Пример 2. Что-то настораживает...
Filename: tests/test_bad.py
Line # Mem usage Increment Line Contents
================================================
6 @profile
7 5.781 MB 0.000 MB def func():
8 21.117 MB 15.336 MB a = range(10**6)
9 21.117 MB 0.000 MB b = int('300')
10 17.301 MB -3.816 MB del a
11 17.309 MB 0.008 MB ctypes.pythonapi.PyInt_ClearFreeList()
12 17.309 MB 0.000 MB del b
13 5.895 MB -11.414 MB ctypes.pythonapi.PyInt_ClearFreeList()
14 5.895 MB 0.000 MB gc.collect(2)
Мы создали кучу Int-ов, затем создали еще один, а потом нашу кучу удалили. В итоге память в ОС не вернулось. Только после того как мы удалили последний Int (и, соответственно, освободился block_list, который он «держал») память наконец-то вернулась на место. Здесь вместо int('300') могли бы быть любые расчеты и прочие операции, которые создают Int-ов.
Можно ли как-то избежать освобождения последнего block_list-а?
Пример 2а. Грязный хак.
Filename: tests/test_bad_hack.py
Line # Mem usage Increment Line Contents
================================================
6 @profile
7 5.789 MB 0.000 MB def func():
8 21.117 MB 15.328 MB a = range(10**6)
9 21.117 MB 0.000 MB b = int('300')
10 17.301 MB -3.816 MB del a
11 17.309 MB 0.008 MB ctypes.pythonapi.PyInt_ClearFreeList()
12 5.785 MB -11.523 MB libc.malloc_trim(0)
13 #del b
14 5.785 MB 0.000 MB ctypes.pythonapi.PyInt_ClearFreeList()
15 5.785 MB 0.000 MB gc.collect(2)
Filename: tests/test_ugly.py
Line # Mem usage Increment Line Contents
================================================
6 @profile
7 5.781 MB 0.000 MB def func():
8 5.781 MB 0.000 MB i = 0
9 5.781 MB 0.000 MB a = []
10 13.094 MB 7.312 MB while i < 10**5: # 10**5 чтобы было быстрее
11 13.094 MB 0.000 MB a.append(i)
12 13.094 MB 0.000 MB i += 1
13 12.711 MB -0.383 MB del a
14 12.711 MB 0.000 MB del i
15 12.719 MB 0.008 MB ctypes.pythonapi.PyInt_ClearFreeList()
16 12.711 MB -0.008 MB libc.malloc_trim(0)
17 12.711 MB 0.000 MB ctypes.pythonapi.PyInt_ClearFreeList()
18 12.711 MB 0.000 MB gc.collect(2)
Здесь нам не помогло ни тщательное удаление всех Int-ов, ни ClearFreeList, ни malloc_trim, ни gc.collect(2). Память осталась в распоряжение malloc-а и в ОС не вернулась.
PS. Примеры запускал под Python 2.7.3 Linux 2.6.32-33 i686. Если Вы можете проверить их под Windows, буду очень признателен.
Я так понимаю дело в реализации PyMalloc, а именно в том что он не хочет отдавать память, которая использовалась для хранения int/float обратно операционной системе после фактического уничтожения объектов, либо использовать ее для других целей, кроме как для хранения новых int/float.
То есть, если мы создадим много объектов (a = range(10**6)), то мы забираем 11.4mb у ОС на хранение int-ов и примерно 4mb на хранение списка. Если после этого удалим «a» (del a), то ОС вернется обратно 4mb, который высвободится из-под списка, но 11.4mb останется в распоряжение питона. Эту память может будет использовать только под новые int-ы/float.
gc.collect([generation])
…
Изменения в версии 2.6. Free list-ы для некоторых встроенных типов очищаются, когда выполняется полная очистка или очистка с максимальной генерацией (2). Ввиду особенности реализации, некоторых объекты во free list-ах могут не удалятся, в частности, int и float.
Насколько я понимаю, в нашем случае это верно. Никакого видимого эффекта после вызова gc.collect(2) не произошло.
Это творческий пересказ решения задачи на leetcode https://leetcode.com/problems/find-the-duplicate-number/editorial/
portal/archive/matrixnet-traffic-forecast/README
Видимо дальше как в анекдоте: «не 500к биткоинов, а 500к рублей. и не российских, а белорусских. и не намайнил, а в тубмочке нашёл»
Объекты Int (как и некоторые другие встроенные типы) поддерживают собственный аллокатор памяти. При необходимости память запрашивается вызовом malloc (в коде используется макрос PyMem_MALLOC, но это просто обертка вокруг malloc; не путать с PyMem_Malloc) блоками по ~1kb.
Есть два связанных списка: block_list — список всех выделенных блоков памяти и free_list — список всех свободных ячеек в этих блоках. Когда создается новый Int, то первым делом (еще есть кэширование значений от -5 до 256, но не будем об этом...) проверяется указатель free_list. Если он не равен null, то Int записывается в свободную ячейку, а free_list укорачивается на один элемент. Если null, то значит, свободных ячеек больше нет. В этом случае вызывается функция fill_free_list, которая запрашивает у malloc новый блок и добавляет его ячейки в список free_list.
Когда количество ссылок на Int становится равно нулю, его адрес добавляется в конец списка free_list.
Посмотреть картинки и подробнее почитать обо всем этом можно здесь: www.laurentluce.com/posts/python-integer-objects-implementation/
Также есть функция PyInt_ClearFreeList, которая отыскивает среди block_list-ов такие, которые полностью состоят из свободных ячеек, вырезает их из списка и вызывает на них malloc_free (опять через обертку PyMem_FREE). Эта функция (в первоначальном название PyInt_CompactFreeList) была добавлена в версии python 2.6 alpha 1 (см. bugs.python.org/issue1953). Следующие комментарий в начале модуля intobject.c явным образом отрицает ее существование, потому что он был написан на 6 лет раньше и с тех пор не обновлялся. Не верьте ему :).
Сама функция PyInt_ClearFreeList регулярно дергается сборщиком мусора. Вот так выглядит ее вызов в версии 2.7.5:
Итак, обещанные примеры.
Для наблюдения за расходом памяти я использовал memory_profiler в связке с psutil. Чтобы освободить память я напрямую дергаю PyInt_ClearFreeList (через ctypes.pythonapi) и, на всякий случай, gc.collect(2), чтобы доказать, что ничего нового не произойдет.
Мы забрали память у ОС, а потом его вернули. Вот бы так было всегда…
Мы создали кучу Int-ов, затем создали еще один, а потом нашу кучу удалили. В итоге память в ОС не вернулось. Только после того как мы удалили последний Int (и, соответственно, освободился block_list, который он «держал») память наконец-то вернулась на место. Здесь вместо int('300') могли бы быть любые расчеты и прочие операции, которые создают Int-ов.
Можно ли как-то избежать освобождения последнего block_list-а?
Здесь мы вызываем функцию malloc_trim из libc и все становиться на место. В примере это сработало, но я бы не стал использовать такой трюк в реальном проекте.
Еще пару примеров можно почитать здесь:
nuald.blogspot.ru/2013/06/memory-reclaiming-in-python.html
bugs.python.org/msg134008
Здесь нам не помогло ни тщательное удаление всех Int-ов, ни ClearFreeList, ни malloc_trim, ни gc.collect(2). Память осталась в распоряжение malloc-а и в ОС не вернулась.
PS. Примеры запускал под Python 2.7.3 Linux 2.6.32-33 i686. Если Вы можете проверить их под Windows, буду очень признателен.
То есть, если мы создадим много объектов (a = range(10**6)), то мы забираем 11.4mb у ОС на хранение int-ов и примерно 4mb на хранение списка. Если после этого удалим «a» (del a), то ОС вернется обратно 4mb, который высвободится из-под списка, но 11.4mb останется в распоряжение питона. Эту память может будет использовать только под новые int-ы/float.
gc.collect([generation])
…
Изменения в версии 2.6. Free list-ы для некоторых встроенных типов очищаются, когда выполняется полная очистка или очистка с максимальной генерацией (2). Ввиду особенности реализации, некоторых объекты во free list-ах могут не удалятся, в частности, int и float.
Насколько я понимаю, в нашем случае это верно. Никакого видимого эффекта после вызова gc.collect(2) не произошло.