python - Cython 容器不会释放内存吗?
问题描述
当我运行以下代码时,我希望一旦foo()
执行,它使用的内存(基本上是 create 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 的情况下释放这些内存,那就太好了。提前致谢。
解决方案
您看到的效果或多或少是内存分配器(可能是 glibc 的默认分配器)的实现细节。glibc 的内存分配器的工作原理如下:
- arenas 满足了对小内存大小的请求,arenas 会根据需要增长/数量会增长。
- 对大内存的请求直接从操作系统获取,但在它们被释放后也直接返回给操作系统。
可以在使用 释放这些领域的内存时进行调整mallopt
,但通常使用内部启发式方法来决定何时/是否应该将内存返回给操作系统 - 我最承认这对我来说是一种黑魔法。
的问题(std::map
情况std::unordered_map
与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 不仅具有将内存返回给 OS 的优势,而且因为 pymalloc 是 8 字节对齐的,而 glibc 的分配器是 32 字节对齐的,因此产生的内存消耗会更小(节点为map[int,int]
40 字节,使用 pymalloc 只需 40.5 字节(连同开销)而 glibc 将需要不少于 64 个字节)。
我对自定义分配器的实现遵循Nicolai M. Josuttis 的示例,并且只实现了真正需要的功能:
%%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
更快,可能需要更少的内存(好的,因为最后一部分的重新散列可能是错误的)。