Python Cython容器是否不释放内存?

Python Cython容器是否不释放内存?,python,memory,memory-leaks,containers,cython,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 f

当我运行下面的代码时,我希望一旦执行了
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的情况下释放内存,那就太好了。提前感谢。

您看到的效果或多或少是内存分配器(可能是glibc的默认分配器)的实现细节。glibc的内存分配器的工作原理如下:

  • arenas满足了对小内存大小的请求,其数量会根据需要增长
  • 对大内存的请求直接从操作系统获取,但一旦释放,也会直接返回到操作系统
我们可以调整这些竞技场的内存何时释放,但通常会使用一个内部启发式算法来决定何时/是否应该将内存返回操作系统——我承认这对我来说是一种黑魔法

std::map
(与
std::unordered_map
的情况类似)的问题是,它不是由一大块立即返回操作系统的内存组成,而是由许多小节点组成(map由libstdc++实现)-所以他们都来自这些领域,启发式决定不将其返回操作系统

当我们使用glibc的分配器时,可以使用非标准函数手动释放内存:

%%cython

cdef extern from "malloc.h" nogil:
     int malloc_trim(size_t pad)

def return_memory_to_OS():
    malloc_trim(0)
%%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
现在只要在每次使用
foo
之后调用
return\u memory\u to\u OS()


上述解决方案快速且不干净,但不便于携带。您想要的是一个自定义分配器,一旦不再使用内存,它就会将内存释放回操作系统。这是一个很大的工作——但幸运的是,我们手头已经有了这样一个分配器:CPython的——自Python2.5以来,它将内存返回给操作系统(即使这意味着)。然而,我们还应该指出pymalloc的一个巨大缺陷——它不是线程安全的,因此它只能用于带有gil的代码

使用pymalloc分配器不仅具有将内存返回操作系统的优点,而且由于pymalloc是8字节对齐的,而glibc的分配器是32字节对齐的,因此产生的内存消耗将更小(映射[int,int]的节点为40字节,这将花费(加上开销)而glibc将需要不少于64个字节)

我对自定义分配器的实现遵循并仅实现真正需要的功能:

%%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

以下是一些基准测试,它们显然不完整,但可能很好地显示了方向(但对于N=3e7而不是N=5e8):

计时通过
%timeit
magic完成,峰值内存使用通过
通过/usr/bin/time-fpeak\u used\u内存:%M python script\u xxx.py

我有点惊讶,pymalloc的性能比glibc分配器好得多,而且似乎内存分配是常规map的瓶颈!也许这就是glibc为支持多线程而必须付出的代价


unordered\u map
更快,可能需要更少的内存(好的,因为重新灰化最后一部分可能是错误的)。

如果查看生成的cpp,您将看到,
m
在堆栈上分配,因此一旦foo()完成,内存将自动释放。是否将释放的内存返回到操作系统是另一个问题-这取决于您的内存分配器,Cython无法为您提供任何帮助。请参阅示例:我认为确认这一点的方法是运行
foo
两次-您不会看到它使用两次memory@ead谢谢分享链接,很不幸,我对这些低级的细节不是很有经验,所以我无法理解其中的大部分内容。但是,公平地说,没有cython/python方法将此内存释放回操作系统吗?@ManishGoel您的地图可能需要超过1GB的内存,我希望您的内存分配器将此内存的大部分返回给操作系统-如果没有,您可能会考虑使用另一个内存分配器。您可以按照上面的链接中所述调整内存分配器的行为(并使用Cython),但这是一个实现细节,对于这个问题来说肯定太宽泛了。
                                   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