Cython容器不释放内存吗?

时间:2019-06-11 09:12:53

标签: python memory memory-leaks containers cython

当我运行以下代码时,我希望一旦执行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的情况下释放此内存,那将是很棒的。预先感谢。

1 个答案:

答案 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更快,并且可能需要更少的内存(好吧,因为重新散列最后一部分可能是错误的)。