python将数值进行关联_深入 Python (4)初识 LongObject

8ab1179bcec27519a4037aafc8576584.png

A glance

  • 精巧的整数类型的结构;
  • Python 为什么可以表示超长整数?
  • 为什么Python 在 intel 64 设备上使用 2^30 进制。
  • Python 整数的理论上限;

Start

整数类型是 Python 最基本的内置对象之一,我们将从这个最简单的对象入手,了解 Python 的面向对对象技术。Python 3 中的整数实现为 PyLongObject,与之关联的几个文件是:

  • Include/longintrepr.h
  • Include/longobject.h
  • Objects/longobject.c

digit 的大小

在 longintrepr.h 定义了 Python 中整数如何表示,其中定义了最关键的类型 digit,这个类型对 Python 表示整数非常重要,最关键的几行代码:

// Include/longintrepr.h
#if PYLONG_BITS_IN_DIGIT == 30
typedef uint32_t digit;
#define PyLong_SHIFT    30

当 PYLONG_BITS_IN_DIGIT 为 30 位的时候,digit 和 PyLong_SHIFT 为 30 位。 PYLONG_BITS_IN_DIGIT 在 Include/pyport.h 中定义:

// Include/pyport.h
#ifndef PYLONG_BITS_IN_DIGIT
#if SIZEOF_VOID_P >= 8
#define PYLONG_BITS_IN_DIGIT 30
#else
#define PYLONG_BITS_IN_DIGIT 15
#endif
#endif

显然 PYLONG_BITS_IN_DIGIT 的大小是根据 SIZEOF_VOID_P 决定的,只要 SIZEOF_VOID_P 不小于 8,PYLONG_BITS_IN_DIGIT 就取 30,否则为 15。 SIZEOF_VOID_P 是什么意思呢?它表示的是操作系统的指针大小,也就是寻址范围,单位是字(8 bit),在 PC/pyconfig.h 可以看到 SIZEOF_VOID_P 的定义,以 Windows 系统为例:

  • 对于 Win32 系统,SIZEOF_VOID_P 为 4,即 4 * 8 = 32 bit;
  • 对于 Win64 系统,SIZEOF_VOID_P 为 8,即 8 * 8 = 64 bit。

因此 digit 在 CPU 寄存器宽度不小于 64 bits 的情况下,为 30 bit,否则只能降低为 15 bits。特别注意 PyLong_SHIFT 在表示任意长度整数时有妙用。

digit 字面意思是数字,而且特指 0~9 中的 1 位数字,可以试想这样一种表示任意整数的方式,将任意整数按照各位 “数字” 保存在一个数组中,比如整数 number = 123456890,16 进制表示为: 0X499602d2,对于 8 位 CPU 是无法直接表示这么大的数字的,这样尝试表示:

091184c14c5926a2e71efd7a3766037a.png

我们将 number 分解为 10 个数字(digit),按位排列保存在一个数组中,这样就可以保存任意位数的整数了。

当然,我们也注意到数组中的每一个元素可以表示的最大正整数为 255,上面的方案只用每一元素表示 0~9,存在巨大的浪费。Python 的方案与此类似,但扩展了对 digit 的定义,在 10 进制中,每一位 digit 的取值范围是 0~9,那么如果是 255(即 2^8) 进制,那么每一位 digit 的范围是 0~255,32 位及以上系统中,Python 就使用了 2^30 进制,那么 digit 的范围就是 0~(2^30 - 1)。具体的后面会更进一步了解到。

为什么 digit 是 30 位而不是 32 位

回顾前面提到 digit 表示超长整数的方案,理论上如果希望最大程度利用内存资源,当 Python 运行在 32 位计算机上,那么每一个 digit 应该用 2^32 进制表示数据,这种考虑是正确的,但忽略了 Python 整数对象的目的,Python 中的整数对象不仅仅用于保存数据,更重要的是进行丰富的数学运算,其中最重要的是乘法运算。

二进制中乘法的实现可以利用“左移” 轻松的实现,比如计算 3 * 4,只需要将 3 左移 2 位实现,很少几条 CPU 指令就可以完成。

4d59619ba1d09e4e04ad995bd27061ce.gif

因此,为了方便计算乘法,我们需要预留一半的空间,再加上多保留 1 位用于计算 “溢出”,在 32 位的情况下,就只能用 INT( (32 - 1)/2 ) = 15 位保存数据,64 位系统则使用 32 位系统数据宽度的 2 倍作为数据大小,即 15 * 2 = 30 bits。这样当两个 Python 整数进行运算的时候可以直接按 digit 计算,并且兼顾了 “溢出位”,结果直接保存在一个寄存器中,以获得最佳性能。

整数对象的结构

整数对象是 PyVarObject 的子集,也就是说一个 longobject 拥有 PyVarObject_HEADER,必然也是 PyObject_HEADER.

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

longobject 看起来像这样:

ea93080908467f8e5ab0392c7eaf5fb7.png

我在图中将所有宏都全部展开了,标注出了头部中不同部分的来源。ob_refcnt 是引用计数;ob_type 是一个指向全局类型对象的指针,对于 longobject 它指向;ob_size 是 longobject 的 digit 数组大小。特别需要注意 ob_size 在 longobject 中存在一些隐式用法,稍后会提到;ob_digit 则是按 2^30 进制分解的 digit 数组。

后面将不再解释 32 位系统与 64 位系统的差异,没有特殊说明,即默认为目前最流行的 64 位系统。

在定义中 ob_digit 的长度明明是 1 啊,为什么还需要 ob_size 呢?实际上 C 语言没有数组越界检查,struct 定义中的数组长度实际并没有准确作用,实际 ob_digit 的长度在创建的时候才会被指定,结构体结尾可变长度的内存会直接覆盖最后的 ob_digit 部分; 另外这种写法可以清晰的表明这里是一个数组。

一个题外话,C 语言中实际上没有真正实现 “数组”,在 C 编译器层面,数组的访问只是简单的 “语法糖”,数组与指针的界限非常模糊,比如下面两段代码可以获得相同的结果:

// 正常
#include <stdio.h>

int main(void) {
    int a[3] = {1,2,3};
    
    printf("%dn", 0[a]);
	return 0;
}

// 奇怪的写法

#include <stdio.h>

int main(void) {
    int a[3] = {1,2,3};
    
    printf("%dn", 0[a]);
	return 0;
}

实际上 C 语言只是将数组访问简单的转换为 *(array_name + index),当然这并不是宏,而是编译器直接实现的。所以对于 C 语言来说,结构体中的数组长度定义并不妨碍它越界访问。需要注意的是,这种做法只能在结构体尾部,如果数组在结构体中间某个位置,那么定义不同的长度,会改变其后各结构体成员的 offset。

ob_size 的隐含信息

longobject 的 ob_size 信息同时包含了以下 3 层信息:

  • 是否为 0;
  • 正负极性;
  • digit 数组长度;

可以用下面的图表示 ob_size 传达的信息:

1e7bafa6fd23cb8d17dd7fd16e374c08.png

ob_digit 表示的其实是无符号整数,数值的极性由 ob_size 的极性表示,比较特殊的是 ob_size 为 0 即表示数值为 0,不需要再去查看 ob_digit 了。ob_size 的绝对值才表示 ob_digit 数组的长度。

这样就将数值的极性标志位放在 ob_size 上,简化了 ob_digit 的实现,实际上前面对 ob_digit 的分析都假定它是无符号整数。

根据这些信息,可以知道其实 Python 整数的上限与 ob_size 的上限有关,在 64 位系统中,ob_size 是一个 int64 类型,其最大正值为 2^63 -1 = 9223372036854775807 ,最小负数为 -2^63 = -9223372036854775808, 2^63 是 ob_digit 数组长度的理论上限,那么 Python 可以认为是一个采用了 2^30 进制的整数计数系统,它能表示的绝对值最大的整数是 -(2^30)^(2^63),这是一个非常大的数字(其实是个负数),可以认为 Python 可以表示任意实用整数了。

实际上创建的 PyLongObject 上限会比上面的理论值略小,因为 Python 中内存大小由 Py_ssize t 表示,其上限是 MAX_LONG_DIGITS = 2^63 - 1,需要先在它的上限中,刨除 PyVarObject_HEADER 的空间 ,剩下的才能用来保存 ob_digit, 所以 ob_digit 的实际上限是 #define MAX_LONG_DIGITS ((PY_SSIZE_T_MAX - offsetof(PyLongObject, ob_digit))/sizeof(digit))。

下一节

整数是最常用的对象,所以 Python 对整数做了一些优化,以提高性能,下一节我们将从整数对象的创建开始探索。

关注我,了解程序员的烧脑日常,还有开源 Python 教程。

https://www.thinkinpython.com/static/upload/20200622/upload_2385f2811af44000121d0c03e6341a2f.png (二维码自动识别)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值