Man*_*oel 5 python memory containers memory-leaks 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()
Run Code Online (Sandbox Code Playgroud)
如果有人能告诉我为什么会出现这种情况以及如何在不重新启动 shell 的情况下释放此内存,那就太好了。提前致谢。
您看到的效果或多或少是内存分配器(可能是 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)
Run Code Online (Sandbox Code Playgroud)
现在只需return_memory_to_OS()
在每次使用后调用foo
.
上述解决方案快速且肮脏,但不可移植。您想要的是一个自定义分配器,一旦不再使用内存,它就会将内存释放回操作系统。这是一项繁重的工作 - 但幸运的是,我们手头已经有了这样一个分配器:CPython 的pymalloc - 从 Python2.5 开始,它会将内存返回给操作系统(即使这有时意味着麻烦)。然而,我们也应该指出 pymalloc 的一个很大的缺陷 - 它不是线程安全的,所以它只能用于带有gil 的代码!
使用 pymalloc-allocator 不仅具有将内存返回给操作系统的优点,而且由于 pymalloc 是 8 字节对齐的,而 glibc 的分配器是 32 字节对齐的,因此产生的内存消耗会更小(节点为 40 字节,使用pymalloc 仅map[int,int]
花费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
Run Code Online (Sandbox Code Playgroud)
现在,一旦完成,大部分已用内存就会返回给操作系统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
Run Code Online (Sandbox Code Playgroud)
以下是一些基准测试,显然不完整,但可能很好地显示了方向(但对于 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
Run Code Online (Sandbox Code Playgroud)
计时是通过%timeit
magic 完成的,峰值内存使用量是通过via /usr/bin/time -fpeak_used_memory:%M python script_xxx.py
.
我有点惊讶,pymalloc 的性能远远优于 glibc-allocator,而且内存分配似乎是通常映射的瓶颈!也许这就是glibc支持多线程必须付出的代价。
unordered_map
速度更快,可能需要更少的内存(好吧,因为重新哈希,最后一部分可能是错误的)。
归档时间: |
|
查看次数: |
3144 次 |
最近记录: |