Контейнер Cython не освобождает память? - PullRequest
2 голосов
/ 11 июня 2019

Когда я запускаю следующий код, я ожидаю, что после выполнения foo() память, используемая им (в основном для создания m), будет освобождена.Однако это не так.Чтобы освободить эту память, мне нужно перезапустить консоль IPython.

%%cython
# distutils: language = c++

import numpy as np
from libcpp.map cimport map as cpp_map

cdef foo():
    cdef:
        cpp_map[int,int]    m
        int i
    for i in range(50000000):
        m[i] = i

foo()

Было бы здорово, если бы кто-то мог сказать мне, почему это так, а также как освободить эту память без перезапуска оболочки.Заранее спасибо.

1 Ответ

2 голосов
/ 12 июня 2019

Эффекты, которые вы видите, являются более или менее подробными сведениями о реализации вашего распределителя памяти (возможный распределитель по умолчанию для glibc).Распределитель памяти в glibc работает следующим образом:

  • запросы на небольшие объемы памяти выполняются с арен, которые растут / число которых увеличивается по мере необходимости.
  • запрос большой памяти напрямую берется из ОС, но также напрямую возвращается в ОС, как только они освобождаются.

Можно настроить, когда память из этих арен освобождается с помощью mallopt, но обычно используется внутренняя эвристика, которая решает, когда / если память должна быть возвращена в ОС - что, я больше всего признаюсь, является своего рода черной магией для меня.

Проблемаиз std::map (и ситуация аналогична std::unordered_map) в том, что он состоит не из большого куска памяти, который будет немедленно возвращен в ОС, а из множества маленьких узлов (карта реализована как Red-Black-Tree от libstdc ++) - так что все они из этих арен, и эвристик решает не возвращать его в ОС.

Поскольку мы используем распределитель glibc, можно использовать нестандартныйФункция malloc_trim для освобождения памяти вручную:

%%cython

cdef extern from "malloc.h" nogil:
     int malloc_trim(size_t pad)

def return_memory_to_OS():
    malloc_trim(0)

и теперь просто вызывайте return_memory_to_OS() после каждого использования foo.


.вышеуказанное решение быстро и грязно, но непортативный.То, что вы хотите иметь, это пользовательский распределитель, который освободит память обратно в ОС, как только она больше не будет использоваться.Это большая работа - но, к счастью, у нас уже есть такой распределитель: CPython pymalloc - начиная с Python2.5 он возвращает память в ОС (даже если это означает иногда проблемы ),Тем не менее, мы также должны указать на большой недостаток pymalloc - он не является потокобезопасным, поэтому его можно использовать только для кода с gil !

Использование pymalloc-allocator не толькопреимущество возврата памяти в ОС, а также потому, что pymalloc выровнен по 8 байтам, в то время как распределитель glibc выровнен по 32 байтам, результирующее потребление памяти будет меньше (узлы размером map[int,int] составляют 40 байт, что будет стоить всего 40,5 байт с pymalloc (вместе с накладными расходами), в то время как glibc потребуется не менее 64 байт).

Моя реализация пользовательского распределителя основана на примере Николая М. Йосуттиса и реализует только действительно необходимые функциональные возможности:

%%cython -c=-std=c++11 --cplus

cdef extern from *:
    """
    #include <cstddef>   // std::size_t
    #include <Python.h>  // pymalloc

    template <class T>
    class pymalloc_allocator {
     public:
       // type definitions
       typedef T        value_type;
       typedef T*       pointer;
       typedef std::size_t    size_type;

       template <class U>
       pymalloc_allocator(const pymalloc_allocator<U>&) throw(){};
       pymalloc_allocator() throw() = default;
       pymalloc_allocator(const pymalloc_allocator&) throw() = default;
       ~pymalloc_allocator() throw() = default;

       // rebind allocator to type U
       template <class U>
       struct rebind {
           typedef pymalloc_allocator<U> other;
       };

       pointer allocate (size_type num, const void* = 0) {
           pointer ret = static_cast<pointer>(PyMem_Malloc(num*sizeof(value_type)));
           return ret;
       }

       void deallocate (pointer p, size_type num) {
           PyMem_Free(p);
       }

       // missing: destroy, construct, max_size, address
       //  -
   };

   // missing:
   //  bool operator== , bool operator!= 

    #include <utility>
    typedef pymalloc_allocator<std::pair<int, int>> PairIntIntAlloc;

    //further helper (not in functional.pxd):
    #include <functional>
    typedef std::less<int> Less;
    """
    cdef cppclass PairIntIntAlloc:
        pass
    cdef cppclass Less:
        pass


from libcpp.map cimport map as cpp_map

def foo():
    cdef:
        cpp_map[int,int, Less, PairIntIntAlloc] m
        int i
    for i in range(50000000):
        m[i] = i

Теперь, львиная доля используемой памяти возвращается в ОС после выполнения foo - в любой операционной системе и распределителе памяти!


Если потребление памятипроблема, можно переключиться на unorder_map, который требует немного меньше памяти.Однако на данный момент unordered_map.pxd не предоставляет доступ ко всем параметрам шаблона, поэтому придется обернуть его вручную:

%%cython -c=-std=c++11 --cplus

cdef extern from *:
    """
    ....

    //further helper (not in functional.pxd):
    #include <functional>
    ...
    typedef std::hash<int> Hash;
    typedef std::equal_to<int> Equal_to;
    """
    ...
    cdef cppclass Hash:
        pass
    cdef cppclass Equal_to:
        pass

cdef extern from "<unordered_map>" namespace "std" nogil:
    cdef cppclass unordered_map[T, U, HASH=*,RPED=*, ALLOC=* ]:
        U& operator[](T&)

N = 5*10**8

def foo_unordered_pymalloc():
    cdef:
        unordered_map[int, int, Hash, Equal_to, PairIntIntAlloc] m
        int i
    for i in range(N):
        m[i] = i

Вот некоторые тесты, которые явно незавершено, но, вероятно, показывает направление довольно хорошо (но для N = 3e7 вместо N = 5e8):

                                   Time           PeakMemory

map_default                        40.1s             1416Mb
map_default+return_memory          41.8s 
map_pymalloc                       12.8s             1200Mb

unordered_default                   9.8s             1190Mb
unordered_default+return_memory    10.9s
unordered_pymalloc                  5.5s              730Mb

Время было сделано с помощью %timeit волшебства и пикового использования памяти с помощью via /usr/bin/time -fpeak_used_memory:%M python script_xxx.py.

Я несколько удивлен, что pymalloc настолько превосходит glibc-allocator, а также кажется, что выделение памяти является узким местом для обычной карты!Возможно, это цена, которую glibc должен заплатить за поддержку многопоточности.

unordered_map быстрее и, возможно, требует меньше памяти (хорошо, из-за перефразирования последняя часть может быть неправильной).

...