整数的大内存占用与sys.getsizeof()结果的比较

4

Python中范围在[1,2^30)的整数对象需要28字节,可以通过使用sys.getsizeof()方法获取并在例如此stackoverflow帖子中解释。

然而,当我使用以下脚本测量内存占用时:

#int_list.py:
import sys

N=int(sys.argv[1])
lst=[0]*N            # no overallocation

for i in range(N):
    lst[i]=1000+i    # ints not from integer pool

通过

/usr/bin/time -fpeak_used_memory:%M python3 int_list.py <N>

我获取到以下峰值内存值(Linux-x64, Python 3.6.2):

   N     Peak memory in Kb        bytes/integer
-------------------------------------------   
   1            9220              
   1e7        404712                40.50 
   2e7        800612                40.52 
   3e7       1196204                40.52
   4e7       1591948                40.52

看起来每个整数对象需要 40.5 字节,比 sys.getsizeof() 得到的结果多出12.5 字节。

额外的 8 字节很容易解释 - 列表 lst 并不包含整数对象本身,而是对它们的引用 - 这意味着需要一个额外的指针,即 8 字节。

然而,其余的 4.5 字节用于什么呢?

以下原因可以被排除:

  • 整数对象的大小可变,但 10^7 小于 2^30,因此所有整数都将是 28 字节。
  • 列表 lst 中没有过度分配,这可以通过 sys.getsizeof(lst) 轻松检查,该函数返回元素数量的 8 倍,加上很小的开销。
2个回答

3

@Nathan的建议出人意料地不是解决方案,因为CPython的longint实现存在一些微妙的细节。根据他的解释,内存占用量为

...
lst[i] = (1<<30)+i

应该仍然是40.52,因为 sys.sizeof(1<<30)32,但测量结果显示为48.56。另一方面,对于

...
lst[i] = (1<<60)+i

尽管 sys.sizeof(1<<60)36,但占用空间仍为 48.56

原因是,sys.getsizeof() 并不能告诉我们一个加法操作的真正内存占用空间,例如 a+b 的结果,其占用空间如下:
  • 1000+i 占用 32 字节
  • (1<<30)+i 占用 36 字节
  • (1<<60)+i 占用 40 字节
这是因为当两个整数在 x_add 中相加时,得出的整数刚开始比 ab 的最大值多一个“数字”(即 4 字节)。
static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;
    ...
    /* Ensure a is the larger of the two: */
    ...
    z = _PyLong_New(size_a+1);  
    ...

添加后,结果被归一化:

 ...
 return long_normalize(z);

};

即可能存在的前导零被舍弃,但内存没有被释放——这四个字节不值得,可以在此处找到函数源代码(链接)


现在,我们可以使用@Nathans的见解来解释为什么(1<<30)+i的占用空间是48.56而不是44.xy:使用的py_malloc分配器使用内存块对齐方式为8字节,这意味着36字节将被存储在大小为40的块中——与结果(1<<60)+i相同(请记住指针的额外8个字节)。


为了解释剩余的0.5字节,我们需要深入了解py_malloc分配器的细节。一个很好的概述是源代码本身,我最后尝试描述它的方式可以在这个SO-post中找到。

简而言之,该分配器在竞技场(arena)中管理内存,每个256MB。当分配竞技场时,将保留内存,但不会提交。只有当所谓的pool被触碰时,我们才将内存视为“已使用”。一个池子大小为4KbPOOL_SIZE),仅用于相同大小的内存块——在我们的情况下是32字节。这意味着peak_used_memory的分辨率为4Kb,并且不能负责那0.5个字节。

然而,这些池必须得到管理,这导致了额外的开销:py_malloc需要每个池的pool_header

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

在我的Linux_64机器上,此结构体的大小为48(称为POOL_OVERHEAD)字节。这个pool_header是池的一部分(一种相当聪明的方式,可以通过运行时内存分配器避免额外的分配),并将取代两个32字节块的位置,这意味着一个池有足够的空间来容纳126个32字节的整数
/* Return total number of blocks in pool of size index I, as a uint. */
#define NUMBLOCKS(I) ((uint)(POOL_SIZE - POOL_OVERHEAD) / INDEX2SIZE(I))

这导致:

  • 1000+i的占用空间为4Kb/126 = 32.51字节,加上指针的额外8个字节。
  • (30<<1)+i需要40字节,这意味着4Kb可以容纳102块,其中一个(当池被分成40字节块时,有剩余的16字节,它们可以用于pool_header)用于pool_header,这导致每个块的大小为4Kb/101=40.55字节(加上8字节指针)。

我们还可以看到,有一些额外的开销,每个整数大约需要0.01个字节 - 对我来说不太重要。


2
"最初的回答":在Python中,每个int对象只需要28个字节,但是Python使用8字节对齐: 内存分配是以8字节为倍数的块进行的。因此,每个int对象实际使用的内存为32个字节。有关更多详细信息,请参见这篇关于Python内存管理的优秀文章。
我目前还没有解释剩余的半个字节,但如果我找到了,我会更新这篇文章。

其实这是一个相当不错的猜测!然而情况更加微妙。请看我的回答(其中也解释了0.5字节)。 - ead

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