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

Does Python not reuse memory here? What does tracemalloc's output mean?

我创建了一个包含一百万个 int 个对象的列表,然后将每个对象替换为其取反的值。 tracemalloc 报告 28 MB 额外内存(每个新 int 对象 28 字节)。为什么? Python 不会为新对象重用垃圾收集的 int 对象的内存吗?还是我误解了 tracemalloc 结果?为什么说这些数字,它们在这里的真正含义是什么?

import tracemalloc

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

输出(Try it online!):

(27999860, 27999972)

如果我用 x = -x 替换 xs[i] = -x(因此新对象而不是原始对象被垃圾收集),输出仅仅是 (56, 196)try it).我 keep/lose?

这两个对象中的哪一个有什么区别

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

简答

tracemalloc 启动太晚,无法跟踪初始内存块,因此它 没有意识到这是一个重用。在您给出的示例中,您释放了 27999860 字节 并分配 27999860 字节,但 tracemalloc 不能 'see' 免费。考虑 以下是稍作修改的示例:

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,但分配器相同),显示:

(35993436, 35993436)
(36000576, 36000716)

我们分配xs后,系统分配了35993436字节,而我们运行 循环我们的净总数为 36000576。这表明内存使用量不是 实际上增加了 28 Mb.

为什么会这样?

Tracemalloc 的工作原理是覆盖标准的内部分配方法 使用 tracemalloc_alloc,以及类似的 free 和 realloc 方法。采取一个 查看 source:

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.) 给一个特殊的table添加trace,这样我们就可以追踪这段记忆

如果我们查看相关的免费功能,它非常相似:

1.) 释放内存

2.) 从 table

中删除跟踪

在您的示例中,您在调用 tracemalloc.start() 之前分配了 xs,因此 此分配的跟踪记录永远不会放在内存跟踪中 table。因此,当您在初始数组数据上调用 free 时,痕迹不会被删除,因此您的分配行为很奇怪。

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

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

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 的指针列表 那恰好是整数。在 xs = list(range(10**6)) 行,我们期望 分配:

  • 1 个内部大小为 1000000 的 PyListObject -- 真实大小:
sizeof(PyObject_HEAD) + sizeof(PyObject *) * 1000000 + sizeof(Py_ssize_t)
(     16 bytes      ) + (    8 bytes     ) * 1000000 + (     8 bytes    )
8000024 bytes
  • 1000000 个 PyObject 整数(底层实现中的 PyLongObject
1000000 * sizeof(PyLongObject)
1000000 * (     28 bytes     )
28000000 bytes

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

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