Эффекты, которые вы видите, являются более или менее подробными сведениями о реализации вашего распределителя памяти (возможный распределитель по умолчанию для 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
быстрее и, возможно, требует меньше памяти (хорошо, из-за перефразирования последняя часть может быть неправильной).