我创建了一个包含100万个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())输出(在网上试试!):
(27999860, 27999972)如果我将xs[i] = -x替换为x = -x (因此新对象而不是原始对象被垃圾收集),输出就是(56, 196) (试试看)。有什么不同,我保留/失去的两个对象中的哪一个?
如果我执行两次循环,它仍然只报告(27992860, 27999972) (试试看)。为什么不是56 MB?第二次运行与第一次运行有什么不同?
发布于 2022-03-15 11:17:19
简短回答
tracemalloc启动得太晚,无法跟踪内存的初始块,因此它没有意识到这是一种重用。在您给出的示例中,您释放了27999860字节,分配了27999860字节,但是tracemalloc无法“看到”空闲。考虑以下稍加修改的示例:
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分配的标准内部方法,以及类似的空闲和realloc方法。偷看一下来源
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中的列表很奇怪。它们实际上是指向单独分配的对象的指针列表。在内部,它们看起来是这样的:
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))上,我们期望分配:
sizeof(PyObject_HEAD) + sizeof(PyObject *) * 1000000 + sizeof(Py_ssize_t)
( 16 bytes ) + ( 8 bytes ) * 1000000 + ( 8 bytes )
8000024 bytesPyLongObject )1000000 * sizeof(PyLongObject)
1000000 * ( 28 bytes )
28000000 bytes总计36000024字节。这个数字看起来很远!
当您在数组中覆盖一个值时,您只需要释放旧值,并更新PyListObject->ob_item中的指针。这意味着数组结构只分配一次,占用8000024字节,并一直保存到程序的末尾。此外,每个分配了1000000个Integer对象,并将引用放在数组中。它们占了28000000字节。一个接一个地释放它们,然后使用内存重新分配循环中的一个新对象。这就是为什么多个循环不会增加内存的原因。
https://stackoverflow.com/questions/71452013
复制相似问题