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

时间:2019-04-09 14:46:34

标签: python python-3.x performance cpython python-internals

[1,2^30)提供的28范围内的Python整数对象需要sys.getsizeof()个字节,例如在this SO-post中进行了解释。

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

#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个字节,即比12.5产生的sys.getsizeof()个字节还要多。

其他8个字节很容易解释-列表lst不包含整数对象,但引用了它们-这意味着一个附加指针,即8个字节,需要。

但是,其他4.5个字节又如何呢?

可以排除以下原因:

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

2 个答案:

答案 0 :(得分:2)

int对象仅需要28个字节,但是Python使用8个字节的对齐方式:内存以8字节大小倍数的块分配。因此,每个int对象使用的实际内存为32个字节。有关更多详细信息,请参见Python memory management上的这篇出色文章。

我还没有剩下的半个字节的解释,但是如果发现一个半字节,我会进行更新。

答案 1 :(得分:1)

@ p @ Nathan的建议出人意料地不是解决方案,因为CPython的longint实现的一些细节。根据他的解释,

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

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

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

尽管事实48.56sys.sizeof(1<<60),足迹仍然是36

原因:sys.getsizeof()不能告诉我们求和结果的实际内存占用量,即a+b,即

  • 1000+i的32个字节
  • (1<<30)+i的36个字节
  • (1<<60)+i的40个字节

之所以会发生这种情况,是因为在x_add中添加两个整数时,所得整数的第一个“数字”(即4个字节)比ab的最大值大:

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);

};

即可能的前导零将被丢弃,但不会释放内存-不足4个字节,可以在here中找到函数的源。


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


要解释剩余的0.5字节,我们需要更深入地研究py_malloc-分配器的细节。 source-code itself是一个很好的概述,我最后一次描述它的方法可以在此SO-post中找到。

简而言之,分配器在舞台上管理内存,每个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是池的一部分(一种非常聪明的方法,可以避免通过cruntime-memory-allocator进行额外分配),它将取代两个32字节块,这意味着一个池具有{ {3}}:

/* Return total number of blocks in pool of size index I, as a uint. */
#define NUMBLOCKS(I) ((uint)(POOL_SIZE - POOL_OVERHEAD) / INDEX2SIZE(I))

哪个会导致:

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

我们还可以看到,还有一些额外的开销负责每个整数8个字节-不够我照顾。