# int
# 内部结构
# _longobject PyLongObject
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
// 对于比较大的整数, Python 将其拆成若干部分,保存在 ob_digit 数组中。
//C 语言中数组长度不是类型信息,我们可以根据实际需要为 ob_digit 数组分配足够的内存,并将其当成长度为 n 的数组操作。
// 数组的长度 由PyObject_VAR_HEAD宏定义中的ob_size存储
};
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
# digit
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit; // 32位
// ...
#elif PYLONG_BITS_IN_DIGIT == 15
typedef unsigned short digit; // 16位
// ...
#endif
# PyLong_Type
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
long_dealloc, /* tp_dealloc */
//...
&long_as_number, /* tp_as_number */
//...
long_new, /* tp_new */
PyObject_Del, /* tp_free */
};
# long_as_number
static PyNumberMethods long_as_number = {
(binaryfunc)long_add, /*nb_add*/
(binaryfunc)long_sub, /*nb_subtract*/
(binaryfunc)long_mul, /*nb_multiply*/
long_mod, /*nb_remainder*/
long_divmod, /*nb_divmod*/
long_pow, /*nb_power*/
(unaryfunc)long_neg, /*nb_negative*/
(unaryfunc)long_long, /*tp_positive*/
(unaryfunc)long_abs, /*tp_absolute*/
(inquiry)long_bool, /*tp_bool*/
(unaryfunc)long_invert, /*nb_invert*/
long_lshift, /*nb_lshift*/
(binaryfunc)long_rshift, /*nb_rshift*/
long_and, /*nb_and*/
long_xor, /*nb_xor*/
long_or, /*nb_or*/
long_long, /*nb_int*/
// ...
};
# 整数布局
- 整数 绝对值 拆分成多个部分,存放于 底层整数数组 ob_digit ;
- 底层数组长度保存在 ob_size 字段,如果整数为负, ob_size 也为负;
- 对于整数 0 ,底层数组为空, ob_size 字段为 0 ;
例如:
对于整数 0 , ob_size 字段等于 0 , ob_digit 数组为空,无需分配。
对于整数 10 ,其绝对值保存于 ob_digit 数组中,数组长度为 1 , ob_size 字段等于 1 。
对于整数 -10 ,其绝对值同样保存于 ob_digit 数组中,但由于 -10 为负数, ob_size 字段等于 -1 。
对于整数 1073741824 ( 2 的 30 次方),由于 ** Python 只使用 32 整数的后 30 位**,需要另一个整数才能存储,整数数组长度为 2 。绝对值这样计算:
2^30*1+2^0*0=1073741824。如果全部 32 位都用来保存绝对值,那么为了保证加法不溢出 (产生进位),需要先强制转换成 64 位类型后在进行计算。但牺牲最高 1 位后,加法运算便不用担心进位溢出了。
那么,为什么 Python 牺牲最高 2 位呢?我猜这是为了和 16 位整数方案统一起来:如果选用 16 位整数作为数组, Python 则只使用其中 15 位。
对于整数 -4294967297 (负的 2 的 32 次方加 1 ),同样要长度为 2 的 ob_digit 数组,但 ob_size 字段为负。绝对值这样计算:
2^30*4+2^0*1=4294967297。
# 内存优化
# 小整数静态对象池
为什么是对象池而不是空闲对象链表:
缓存池比空闲链表更快,因为对象是已经初始化好了的,直接拿过来就能用。空闲链表只是解决了内存频繁分配的问题,省去了申请内存的开销,但仍然需要重新初始化对象。另外,由于int对象是不定长的,实现空内存链表的话,也只能保存固定大小的一类,比如只缓存底层数组只有一个元素的对象,或者安装底层数组长度,分类缓存。
同float一样,为了避免运行时大量整数对象被创建销毁(例如for i in range(100)),严重拖慢性能,Python提供了一个小整数池的解决方案:提前将常用的整数对象创建好,以备后用。
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257 // 正数个数,默认从0开始
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5 // 负数个数
#endif
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; // 小整数池
# 常用整数优化
注意在3.11中如下的解释器层级的优化是存在的,但是在更低版本的python中可能不成立。
虽然python仅对[-5,256]的小整数建立常量池,但是对于超过这个范围的常用整数也做了一定优化:对于在编译时就可以确定值相同的变量让指向同一个对象。
例如下面例子中的1235,都是在执行前就可以确定,因此所有数实际都指向到同一个对象,但是例子4由于要进行运算才能知道值因此没有指向同一个对象。
a = 10000
b = 10000
print(a is b) # True
a = 10000
b = 10000 + 0
print(a is b) # True
a = 10000
b = a
print(a is b) # True
a = 10000
b = a + 0
print(a is b) # False
a = 10 ** 3
b = 10 * 100
c = 9 * 111 + 1
d = 1000
print(a is b is c is d) # True
# 奇巧淫技
可以dis一下整数的+=,可以发现其实际上分为了两个字节码,即a+=b实际上和a=a+b是一致的,都是先计算结果然后赋值给a,既然有了两个字节码,就有可能出现racing的情况,但是我们可以用一种奇特的方法实现整数不会racing的+1操作:
>>> dis.dis("n=1;n+=1;")
1 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (n)
4 LOAD_NAME 0 (n)
6 LOAD_CONST 0 (1)
8 INPLACE_ADD
10 STORE_NAME 0 (n)
12 LOAD_CONST 1 (None)
14 RETURN_VALUE
# 这段代码来自asyncio
# 生成新的 task 时的命名计数器
# 这里不采用 +=1 的操作是因为协程并非线程安全
# 通过迭代器不断的向后计数,可以完美的保证线程安全
_task_name_counter = itertools.count(1).__next__
# 使用方式
if name is None:
self._name = f'Task-{_task_name_counter()}'