当我运行以下代码时,我希望一旦执行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()
如果有人能告诉我为什么会发生这种情况,以及如何在不重新启动Shell的情况下释放此内存,那将是很棒的。预先感谢。
答案 0 :(得分:2)
您所看到的效果或多或少是您的内存分配器(可能是glibc的默认分配器)的实现细节。 glibc的内存分配器的工作方式如下:
当使用mallopt
释放那些区域中的内存时,可以进行调整,但是通常使用内部启发式方法来决定何时/是否应该将内存返回给OS-我最承认的是黑色对我来说很神奇。
std::map
的问题(std::unordered_map
的情况与此类似)是,它不包含将立即返回给操作系统的大内存块,而是很多小节点(地图由libstdc ++实现为Red-Black-Tree)-因此它们都来自那些领域,启发式方法决定不将其返回给操作系统。
当我们使用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
。
以上解决方案快捷,肮脏,但不可移植。您想要的是一个自定义分配器,该分配器将在不再使用内存时将其释放回OS。这是很多工作-但幸运的是,我们已经有了这样的分配器:CPython的pymalloc-自Python2.5起,它就将内存返回给OS(即使这意味着sometimes trouble)。但是,我们还应该指出pymalloc的一大缺陷-它不是线程安全的,因此只能用于具有gil的代码!
使用pymalloc-allocator不仅具有将内存返回给OS的优势,而且因为pymalloc是8byte对齐的,而glibc的分配器是32byte对齐的,因此导致的内存消耗将较小(map[int,int]
的节点为40字节费用为only 40.5 bytes with pymalloc(连同开销),而glibc将需要不少于64个字节)。
我对自定义分配器的实现遵循Nicolai M. Josuttis' example,并且仅实现真正需要的功能:
%%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
完成后,在所有操作系统和内存分配器上,大部分已使用的内存都将返还给OS!
如果内存消耗成问题,则可以切换到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
更快,并且可能需要更少的内存(好吧,因为重新散列最后一部分可能是错误的)。