Python中id(1)和id(2)返回的内存地址为什么相差32?
测试环境 Python 3.6.4,现象:
>>> id(1)
4489990816
>>> id(2)
4489990848
我的想法:相差的 32 应该是 32 bits(存疑)。id() 在 CPython 中返回的是 1 和 2 在内存中的地址,由于 Python 每个对象都有标准对象头,类型指针和引用计数等信息,所以不能简单地用 -5 ~ 255 这个范围内的整数所占的最小空间去存储,4 bytes 的空间内(感觉有点小?)存储了一些其他信息。因为没有去了解 CPython 具体的实现,不知道自己想的对不对。然后就又有个一个问题:
>>> import sys
>>> sys.getsizeof(1)
28
文档中说 sys.getsizeof() "Return the size of an object in bytes." 并且 "Only the memory consumption directly attributed to the object is accounted for, not the memory consumption of objects it refers to." 请问这句话中的 directly attributed to 和 refers to 应该怎么理解?(听起来像英语阅读理解…_(:3 」∠)_)这个 size 真的是 bytes 么?
Python中id(1)和id(2)返回的内存地址为什么相差32?
实际上 Python 中的 int 并不是定长的。。。
>>> import sys
>>> sys.getsizeof(1)
28
>>> sys.getsizeof(100000000000000000000000)
36
>>> sys.getsizeof(1000000000000000000000000000000000000000000000000)
48
>>> sys.getsizeof(1000000000000000000000000000000000000000000000000000000000000000)
52
这其实和Python解释器对小整数的缓存机制有关。在CPython中,为了优化性能,解释器会预先创建并缓存一个范围内的小整数对象(通常是-5到256)。当你使用这些数字时,解释器直接返回缓存对象的引用,而不是每次都创建新对象。
所以,id(1)和id(2)返回的是这两个预分配对象在内存中的地址。这两个对象在内存中是连续存储的,它们地址的差值(32字节)就是CPython中一个PyLongObject结构体实例在64位系统上典型的内存大小。
你可以用下面这个简单的代码来验证:
import sys
# 获取整数1和2的对象大小
size_of_1 = sys.getsizeof(1)
size_of_2 = sys.getsizeof(2)
print(f"Size of integer 1: {size_of_1} bytes")
print(f"Size of integer 2: {size_of_2} bytes")
print(f"Difference in id: {id(2) - id(1)}")
print(f"Are they equal to object size? {id(2) - id(1) == size_of_1}")
在64位Python 3.8+的典型输出会是:
Size of integer 1: 28 bytes
Difference in id: 32
Are they equal to object size? False
注意这里显示28字节但地址差32字节,是因为内存对齐(通常按8字节对齐)导致的。这个差值就是这两个缓存整数对象在内存中的实际存储间距。
简单说,你看到的是两个被缓存对象的存储间距。
#1 确实 貌似只要内存允许 可以无限变大…
>>> sys.getsizeof(2**999999)
133360
。。。
你不应该假设连续制造的两个对象的地址也是“接近”的。
后面 Only … to 这句话是想告诉你如下事实:考虑 A 对象具有 B 字段,B 字段的值是(指向) C 对象(的引用),A 对象的 size 不非要大于 C 对象的 size —— C 对象的 size 不会算入 A 对象的 size。类比到 C 的话
typedef struct { void *member1, *member2, *member3, *member4; } c_t;
typedef struct { c_t *b; } a_t;
则 sizeof(a_t) 是 sizeof(void *) 而不是 sizeof(void *) + sizeof(c_t)。
CPython 里面所有的小整数都是被预初始化,做成一个池的。
id(1)和 id(2)相差的是 32bytes 不是 32bits
在 CPython 的实现里小整数缓存是这样定义的:
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
在我的 64 位机器上 sizeof(PyLongObject)是 32bytes 因此 id(1)和 id(2)相差了 32bytes
在 CPython 的实现里也可以找到 PyLongObject 的定义:
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
struct _longobject {
----PyObject_VAR_HEAD
----digit ob_digit[1];
};
在我的机器上 PyObject_VAR_HEAD 占 24bytes, digit ob_digit[1]占 4bytes。
可能由于内存对齐的缘故, PyLongObject 实际占用了 32bytes 而不是 28bytes。
首先更正小整数对象池 small_ints 的区间应该是 [-5, 257),我正文里写错了。
#3 因为不熟悉 C,我目前只能大概理解你后面的意思,之后我会再好好想想。关于假设连续对象的地址接近这个问题,在 [-5, 257) 内的各个数都是差 32,应该是因为先开辟出一整块空间( PyIntBlock ),然后构建的链表,所以是连续的(我刚刚找到的链接里有提到)。然而验证又涉及到阅读具体实现的 C 源代码,看来还是绕不开…
#4 「小整数」这个关键词找到了有用的信息,我之前知道是被优化了的,但好像搜的关键词不太对
现在看到了这个 https://foofish.net/python_int_implement.html
#5 非常感谢!!
1. 小整数缓存用的是数组不是链表
2. 6 楼最后提到的链接是 Python2 的实现
由于我写的比较多,而且回复没有 md 所以我开辟了一个主题
https://www.v2ex.com/t/463799#reply0
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-120
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-40
>>> id(100000000000000000000003)-id(100000000000000000000000)
-80
-40 似乎是内存分配器正好分配了连续的内存空间而产生的巧合。
其实并没有那么简单,你再看看下面的几行输出
>>> id(100000000000000000000000)
1932290411216
>>> id(100000000000000000000001)
1932290411176
>>> id(100000000000000000000002)
1932290411136
>>> id(100000000000000000000003)
1932290411096
>>> id(100000000000000000000000)
139793637212488
>>> id(100000000000000000000000)
139793637212448
>>> id(100000000000000000000000)
139793637212408
>>> id(100000000000000000000000)
139793637212368
所以呢
只是说这个问题就很有趣了,涉及到表达式执行顺序,内存分配器策略和垃圾回收时机了,只要频率够高还能跑出来更加诡异的数字
>>> id(100000000000000000000001)-id(100000000000000000000000)
-1160
>>> id(100000000000000000000001)-id(100000000000000000000000)
-160
>>> id(100000000000000000000001)-id(100000000000000000000000)
-1000
>>> id(100000000000000000000001)-id(100000000000000000000000)
1160
>>> id(100000000000000000000001)-id(100000000000000000000000)
1120
>>> id(100000000000000000000001)-id(100000000000000000000000)
1200


