为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

慕田峪7331174 2022-01-05 10:05:51
范围内的 Python-Integer-objects[1,2^30)需要28字节,如sys.getsizeof()在此 SO-post 中提供和解释的那样。但是,当我使用以下脚本测量内存占用时:#int_list.py:import sysN=int(sys.argv[1])lst=[0]*N            # no overallocationfor 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字节大。list 中没有过度分配lst,可以很容易地检查sys.getsizeof(lst)它,它产生8了元素数量的倍数,加上非常小的开销。
查看完整描述

2 回答

?
暮色呼如

TA贡献1853条经验 获得超9个赞

longint@Nathan的建议令人惊讶地不是解决方案,因为 CPython 的实现有一些微妙的细节。根据他的解释,内存占用为


...

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

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


...

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

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


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


32 字节 1000+i

36 字节用于 (1<<30)+i

40 字节用于 (1<<60)+i

发生这种情况,因为当两个整数的相加x_add,所得到的整数,在具有第一个“数字”,即4个字节,超过最大的a和b:


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 个字节不值得,函数的来源可以在这里找到。


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


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


简而言之,分配器管理 arenas 中的内存,每个内存为 256MB。分配 arena 时,会保留内存,但不会提交。我们将内存视为“已使用”,只有当一个所谓的pool被触及时。池4Kb很大 ( POOL_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字节块,这意味着池有用于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))

这导致:


4Kb/126 = 32.51的字节足迹1000+i,加上额外的 8 个字节的指针。

(30<<1)+i需要40字节,这意味着4Kb有102块的地方,其中一个(16当池划分为40-bytes块时还有剩余的字节,它们可以用于pool_header)用于pool_header,这导致4Kb/101=40.55字节(加上8字节指针)。

我们还可以看到,还有一些额外的开销,负责 ca。0.01每个整数字节 - 不足以让我关心。


查看完整回答
反对 回复 2022-01-05
?
智慧大石

TA贡献1946条经验 获得超3个赞

int对象只需要 28 个字节,但 Python 使用 8 字节对齐:内存分配在大小为 8 个字节的倍数的块中。所以每个int对象实际使用的内存是 32 字节。有关更多详细信息,请参阅这篇关于Python 内存管理的优秀文章。

我还没有对剩余的半字节做出解释,但如果我找到了,我会更新它。


查看完整回答
反对 回复 2022-01-05
  • 2 回答
  • 0 关注
  • 267 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号