【问题标题】:Does Cython container not release memory?Cython 容器不会释放内存吗?
【发布时间】:2019-10-25 16:08:39
【问题描述】:

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

【问题讨论】:

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

标签: python memory memory-leaks containers cython


【解决方案1】:

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

  • 对小内存大小的请求从 arena 得到满足,arenas 会根据需要增长/其数量会增长。
  • 对大内存的请求直接从操作系统获取,但在释放后也直接返回给操作系统。

当使用mallopt 释放来自这些领域的内存时,可以进行调整,但通常会使用内部启发式方法来决定何时/是否应该将内存返回给操作系统 - 我最承认这是一种黑魔法我。

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)

现在只需在每次使用 foo 后调用 return_memory_to_OS()。


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

使用 pymalloc-allocator 不仅有将内存返回给 OS 的优点,而且因为 pymalloc 是 8 字节对齐的,而 glibc 的分配器是 32 字节对齐的,因此产生的内存消耗会更小(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 完成,大部分已用内存将返回给操作系统 - 在任何操作系统和内存分配器上!


如果内存消耗是一个问题,可以切换到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 更快,可能需要更少的内存(好的,因为重新散列最后一部分可能是错误的)。

【讨论】:

    猜你喜欢
    • 2016-07-15
    • 2018-03-09
    • 2012-09-24
    • 2014-11-17
    • 1970-01-01
    • 1970-01-01
    • 2011-12-27
    • 1970-01-01
    • 2012-11-21
    相关资源
    最近更新 更多