你看到的效果更多或更少是你的内存分配器(可能是glibc的默认分配器)的实现细节。 glibc的内存分配器工作原理如下:
- 小内存大小的请求从竞技场中满足,该竞技场根据需要增长/其数量增长。
- 大内存请求直接从操作系统中获取,但一旦它们被释放,也直接返回给操作系统。
可以使用
mallopt
调整来释放这些竞技场中的内存,但通常会使用内部启发式算法来决定何时/是否将内存返回给操作系统 - 对我来说,这似乎有点像黑魔法。
std :: map
的问题(
std :: unordered_map
的情况类似)在于,它不由一个立即返回给操作系统的大内存块组成,而是由许多小节点组成(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)
每次使用 foo
后,只需调用 return_memory_to_OS()
即可。
上述解决方案虽然简单易行,但不具备可移植性。你需要拥有一个自定义内存分配器,它能够在不再使用时立即将内存释放回操作系统。这项工作量很大,但幸运的是,我们已经有了这样一个内存分配器:CPython 的 pymalloc - 自 Python2.5 起,它会将内存返回给操作系统(即使这可能会导致部分问题)。但是,我们还应指出 pymalloc 的一个缺陷——它不是线程安全的,因此只能用于带有 GIL 的代码!
使用 pymalloc 分配器不仅可以将内存返回给操作系统,而且由于 pymalloc 是 8 字节对齐的,而 glibc 的分配器是 32 字节对齐的,所以 resulting memory consumption 将更小(map [int,int]
的节点占用40字节,将花费仅为40.5字节与开销一起使用pymalloc,而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=5e8而言,这些结果基于N=3e7):
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
魔法实现的,峰值内存使用量是通过/usr/bin/time -fpeak_used_memory:%M python script_xxx.py
实现的。
让我有些惊讶的是,pymalloc比glibc-allocator表现得要好这么多,而且似乎内存分配是常规映射的瓶颈!也许这就是glibc为支持多线程所必须付出的代价。
unordered_map
更快,可能需要更少的内存(好吧,由于重新哈希,最后一部分可能是错误的)。
m
是在栈上分配的,因此一旦foo()完成,内存就会自动释放。这个被释放的内存是否返回给操作系统是另一个问题 - 这取决于你的内存分配器,而不是Cython能帮助你的。 - eadfoo
- 您不会看到它使用双倍的内存。 - DavidW