Cython容器不释放内存吗?

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


2
如果你查看生成的cpp文件,你会发现m是在栈上分配的,因此一旦foo()完成,内存就会自动释放。这个被释放的内存是否返回给操作系统是另一个问题 - 这取决于你的内存分配器,而不是Cython能帮助你的。 - ead
我认为确认这一点的方法是运行两次 foo - 您不会看到它使用双倍的内存。 - DavidW
@ead 感谢分享链接,不幸的是,我在这些低级细节方面经验不太丰富,所以我无法理解其中很多内容。但是,可以说没有Cython/Python方法将此内存释放回操作系统吗? - Manish Goel
当我在Linux上测试时,我看到与OP相同的情况-内存似乎在函数之后没有被释放(但第二次调用函数不会使用更多内存)。如果是我的话,我不会担心它。 - DavidW
@DavidW 问题是,实际函数需要超过200GB的内存。因此,我需要释放它,以便机器上的其他进程可以继续工作。 - Manish Goel
显示剩余2条评论
1个回答

5
你看到的效果更多或更少是你的内存分配器(可能是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更快,可能需要更少的内存(好吧,由于重新哈希,最后一部分可能是错误的)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接