Как стать автором
Обновить
23
0
Сергей Романов @sleepingonee

Пользователь

Отправить сообщение

Это творческий пересказ решения задачи на leetcode https://leetcode.com/problems/find-the-duplicate-number/editorial/

!!**NDA, не пишим, не рассказываем, скрины не выкладываем**!!

portal/archive/matrixnet-traffic-forecast/README

Уже пришло уточнение: «не техдиректор, а разработчик»:
Видимо дальше как в анекдоте: «не 500к биткоинов, а 500к рублей. и не российских, а белорусских. и не намайнил, а в тубмочке нашёл»
В примере 2a в начале модуля была строка:

libc = ctypes.CDLL('libc.so.6')

Этот комментарий не верен. Intobject.c получает память напрямую через malloc и также ее освобождает. См. новый комментарий выше.
Похоже, я был зомбирован статьями и комментариями в интернете, которые уже успели устареть. Почитал исходный код и кое-чего уяснил. Я попробую проиллюстрировать свои открытия на примерах, но сначала немного теории…

Объекты 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 лет раньше и с тех пор не обновлялся. Не верьте ему :).

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)


Здесь мы вызываем функцию malloc_trim из libc и все становиться на место. В примере это сработало, но я бы не стал использовать такой трюк в реальном проекте.
Еще пару примеров можно почитать здесь:
nuald.blogspot.ru/2013/06/memory-reclaiming-in-python.html
bugs.python.org/msg134008

Пример 3. Грустный..
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):

gc.collect([generation])

Изменения в версии 2.6. Free list-ы для некоторых встроенных типов очищаются, когда выполняется полная очистка или очистка с максимальной генерацией (2). Ввиду особенности реализации, некоторых объекты во free list-ах могут не удалятся, в частности, int и float.

Насколько я понимаю, в нашем случае это верно. Никакого видимого эффекта после вызова gc.collect(2) не произошло.
Спасибо, я, конечно, думал про целые числа (как противоположность дробным), а про «простые».

Информация

В рейтинге
5 092-й
Зарегистрирован
Активность