首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Python在这里不重用内存吗?tracemalloc的输出是什么意思?

Python在这里不重用内存吗?tracemalloc的输出是什么意思?
EN

Stack Overflow用户
提问于 2022-03-12 17:55:45
回答 1查看 462关注 0票数 6

我创建了一个包含100万个int对象的列表,然后用其负值替换每个对象。tracemalloc报告了28 MB的额外内存(每个新的int对象有28个字节)。为什么?Python没有为新对象重用垃圾收集的int对象的内存吗?还是我误解了tracemalloc的结果?为什么要说这些数字,它们到底是什么意思?

代码语言:javascript
复制
import tracemalloc

xs = list(range(10**6))
tracemalloc.start()
for i, x in enumerate(xs):
    xs[i] = -x
print(tracemalloc.get_traced_memory())

输出(在网上试试!):

代码语言:javascript
复制
(27999860, 27999972)

如果我将xs[i] = -x替换为x = -x (因此新对象而不是原始对象被垃圾收集),输出就是(56, 196) (试试看)。有什么不同,我保留/失去的两个对象中的哪一个?

如果我执行两次循环,它仍然只报告(27992860, 27999972) (试试看)。为什么不是56 MB?第二次运行与第一次运行有什么不同?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-03-15 11:17:19

简短回答

tracemalloc启动得太晚,无法跟踪内存的初始块,因此它没有意识到这是一种重用。在您给出的示例中,您释放了27999860字节,分配了27999860字节,但是tracemalloc无法“看到”空闲。考虑以下稍加修改的示例:

代码语言:javascript
复制
import tracemalloc

tracemalloc.start()

xs = list(range(10**6))
print(tracemalloc.get_traced_memory())
for i, x in enumerate(xs):
    xs[i] = -x
print(tracemalloc.get_traced_memory())

在我的机器上(python 3.10,但是相同的分配器),它显示:

代码语言:javascript
复制
(35993436, 35993436)
(36000576, 36000716)

在分配xs之后,系统已经分配了35993436字节,在运行循环之后,我们的净总数为36000576。这表明内存使用量并没有增加28 Mb。

它为什么会这样表现?

Tracemalloc的工作方式是重写使用tracemalloc_alloc分配的标准内部方法,以及类似的空闲和realloc方法。偷看一下来源

代码语言:javascript
复制
static void*
tracemalloc_alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
{
    PyMemAllocatorEx *alloc = (PyMemAllocatorEx *)ctx;
    void *ptr;

    assert(elsize == 0 || nelem <= SIZE_MAX / elsize);

    if (use_calloc)
        ptr = alloc->calloc(alloc->ctx, nelem, elsize);
    else
        ptr = alloc->malloc(alloc->ctx, nelem * elsize);
    if (ptr == NULL)
        return NULL;

    TABLES_LOCK();
    if (ADD_TRACE(ptr, nelem * elsize) < 0) {
        /* Failed to allocate a trace for the new memory block */
        TABLES_UNLOCK();
        alloc->free(alloc->ctx, ptr);
        return NULL;
    }
    TABLES_UNLOCK();
    return ptr;
}

我们看到新的分配器做了两件事:

1.)呼叫“旧”分配器以获得内存。

2.)向特殊表添加跟踪,以便跟踪此内存

如果我们看看相关的自由函数,它是非常相似的:

1.)释放记忆

2.)从表中删除跟踪

在您的示例中,您在调用tracemalloc.start()之前分配了xs,因此该分配的跟踪记录永远不会放在内存跟踪表中。因此,当您对初始数组数据调用空闲时,跟踪不会被删除,从而导致您的奇怪的分配行为。

为什么总内存使用量是36000000字节而不是28000000字节?

python中的列表很奇怪。它们实际上是指向单独分配的对象的指针列表。在内部,它们看起来是这样的:

代码语言:javascript
复制
typedef struct {
    PyObject_HEAD
    Py_ssize_t ob_size;

    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     */
    Py_ssize_t allocated;
} PyListObject;

PyObject_HEAD是一个宏,它扩展到所有python变量都具有的一些头信息。它只有16个字节,包含指向类型数据的指针。

重要的是,整数列表实际上是指向PyObjects的指针列表,这些指针恰好是ints。在行xs = list(range(10**6))上,我们期望分配:

  • 具有内部大小1000000的1 PyListObject --真实大小:
代码语言:javascript
复制
sizeof(PyObject_HEAD) + sizeof(PyObject *) * 1000000 + sizeof(Py_ssize_t)
(     16 bytes      ) + (    8 bytes     ) * 1000000 + (     8 bytes    )
8000024 bytes
  • 1000000 PyObject in (底层植入中的PyLongObject )
代码语言:javascript
复制
1000000 * sizeof(PyLongObject)
1000000 * (     28 bytes     )
28000000 bytes

总计36000024字节。这个数字看起来很远!

当您在数组中覆盖一个值时,您只需要释放旧值,并更新PyListObject->ob_item中的指针。这意味着数组结构只分配一次,占用8000024字节,并一直保存到程序的末尾。此外,每个分配了1000000个Integer对象,并将引用放在数组中。它们占了28000000字节。一个接一个地释放它们,然后使用内存重新分配循环中的一个新对象。这就是为什么多个循环不会增加内存的原因。

票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71452013

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档