PEP 353 – 使用 ssize_t 作为索引类型
- 作者:
- Martin von Löwis <martin at v.loewis.de>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2005年12月18日
- Python 版本:
- 2.5
- 历史记录:
摘要
在 Python 2.4 中,序列的索引被限制为 C 类型 int。在 64 位机器上,序列因此无法使用完整的地址空间,并且被限制为 2**31 个元素。本 PEP 提出更改这一点,引入特定于平台的索引类型 Py_ssize_t。所提议更改的实现位于 http://svn.python.org/projects/python/branches/ssize_t。
基本原理
64 位机器变得越来越流行,主内存的大小也超过了 4GiB。在这样的机器上,Python 当前受到限制,因为序列(字符串、Unicode 对象、元组、列表、array.arrays 等)不能包含超过 2Gi 个元素。
如今,很少有机器拥有足以表示更大列表的内存:由于每个指针都是 8B(在 64 位机器中),因此仅保存此类列表的指针就需要 16GiB;如果列表中包含数据,则内存消耗会进一步增加。但是,有三种容器类型,用户今天要求对其进行改进
- 字符串(目前限制为 2GiB)
- mmap 对象(类似;此外,系统通常不会同时将整个对象保存在内存中)
- Numarray 对象(来自 Numerical Python)
由于所提议的更改将在 64 位机器上导致不兼容性,因此应在这些机器尚未广泛使用时进行(即,尽早进行)。
规范
引入了一种新的类型 Py_ssize_t,其大小与编译器的 size_t 类型相同,但带符号。在可用时,它将是 ssize_t 的 typedef。
所有容器类型的长度字段的内部表示都从 int 更改为 ssize_t,对于标准分发中包含的所有类型都是如此。特别是,PyObject_VAR_HEAD 更改为使用 Py_ssize_t,影响所有使用该宏的扩展模块。
所有索引和长度参数以及结果的出现都更改为使用 Py_ssize_t,包括类型对象中的序列槽和缓冲区接口。
引入了新的转换函数 PyInt_FromSsize_t 和 PyInt_AsSsize_t。如果值超过 LONG_MAX,则 PyInt_FromSsize_t 将透明地返回一个 long int 对象;PyInt_AsSsize_t 将透明地处理 long int 对象。
引入了新的函数指针类型定义 ssizeargfunc、ssizessizeargfunc、ssizeobjargproc、ssizessizeobjargproc 和 lenfunc。缓冲区接口函数类型现在称为 readbufferproc、writebufferproc、segcountproc 和 charbufferproc。
为 PyArg_ParseTuple、Py_BuildValue、PyObject_CallFunction 和 PyObject_CallMethod 引入了新的转换代码“n”。此代码在 Py_ssize_t 上运行。
如果在包含 Python.h 之前定义了宏 PY_SSIZE_T_CLEAN,则转换代码“s#”和“t#”将输出 Py_ssize_t,如果未定义该宏,则继续输出 int。
在需要从 size_t/Py_ssize_t 转换为 int 的地方,将根据具体情况选择转换策略(请参阅下一节)。
为了防止将假定 32 位大小类型的扩展模块加载到具有 64 位大小类型的解释器中,Py_InitModule4 重命名为 Py_InitModule4_64。
转换指南
模块作者可以选择是否在其代码中支持此 PEP;如果他们支持它,则可以选择不同级别的兼容性。
如果模块未转换为支持此 PEP,则它将在 32 位系统上继续按修改前的方式工作。在 64 位系统上,可能会发出编译时错误和警告,并且如果忽略警告,该模块可能会导致解释器崩溃。
模块的转换可以尝试继续使用 int 索引,也可以在整个过程中使用 Py_ssize_t 索引。
如果模块应继续使用 int 索引,则在调用返回 Py_ssize_t 或 size_t 的函数时必须小心,特别是对于返回对象长度的函数(包括 strlen 函数和 sizeof 运算符)。一个好的编译器会在 Py_ssize_t/size_t 值被截断为 int 时发出警告。在这些情况下,可以使用三种策略
- 静态确定大小永远不会超过 int(例如,在获取 struct 的 sizeof 或文件路径名的 strlen 时)。在这种情况下,编写
some_int = Py_SAFE_DOWNCAST(some_value, Py_ssize_t, int);
这将在调试模式下添加一个断言,以确保该值确实适合 int,并在其他情况下仅添加一个强制转换。
- 静态确定该值不应溢出 int,除非某个地方的 C 代码存在错误。测试该值是否小于 INT_MAX,如果不是,则引发 InternalError。
- 否则,检查该值是否适合 int,如果不适合,则引发 ValueError。
对于 tp_as_sequence 槽也必须注意这一点,此外,这些槽的签名会发生变化,并且必须显式地重新强制转换(例如,从 intargfunc 到 ssizeargfunc)。可以通过以下测试实现与以前 Python 版本的兼容性
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif
然后在代码的其余部分使用 Py_ssize_t。对于 tp_as_sequence 槽,可能需要其他类型定义;或者,通过替换
PyObject* foo_item(struct MyType* obj, int index)
{
...
}
与
PyObject* foo_item(PyObject* _obj, Py_ssize_t index)
{
struct MyType* obj = (struct MyType*)_obj;
...
}
可以完全省略强制转换;然后,foo_item 的类型应与所有 Python 版本中的 sq_item 槽匹配。
如果模块应扩展为使用 Py_ssize_t 索引,则应查看 int 类型的所有用法,以查看是否应将其更改为 Py_ssize_t。编译器将有助于找到这些位置,但仍然需要手动审查。
对于 PyArg_ParseTuple 调用,必须特别注意:需要检查所有这些调用是否有 s# 和 t# 转换器,并且如果相应地更新了这些调用,则必须在包含 Python.h 之前定义 PY_SSIZE_T_CLEAN。
Fredrik Lundh 编写了一个 扫描程序,用于检查 C 模块的代码以使用其签名已更改的 API。
讨论
为什么不使用 size_t
最初尝试实现此功能时试图使用 size_t。很快发现这行不通:Python 在许多地方使用负索引(表示从末尾开始计数)。即使在可以使用 size_t 的地方,也需要对代码进行太多重新制定,例如在以下循环中
for(index = length-1; index >= 0; index--)
如果 index 从 int 更改为 size_t,则此循环将永远不会终止。
为什么不使用 Py_intptr_t
从概念上讲,Py_intptr_t 和 Py_ssize_t 是不同的东西:Py_intptr_t 需要与 void* 大小相同,而 Py_ssize_t 与 size_t 大小相同。这些可能不同,例如在指针具有段和偏移量的机器上。在当前的平面地址空间机器上,没有区别,因此在所有实际用途上,Py_intptr_t 也能正常工作。
这不会破坏很多代码吗?
通过所提出的更改,代码破坏非常小。在 32 位系统上,不会出现任何代码破坏,因为 Py_ssize_t 只是 int 的 typedef。
在 64 位系统上,编译器会在许多地方发出警告。如果忽略这些警告,则代码将继续工作,只要容器大小不超过 2**31,即它将几乎像当前一样工作。此语句有两个例外:如果扩展模块实现了序列协议,则必须对其进行更新,否则调用约定将不正确。另一个例外是通过指针(而不是返回值)输出 Py_ssize_t 的地方;这最显著地适用于编解码器和切片对象。
如果对代码进行转换,则相同的代码可以继续在早期 Python 版本上运行。
这不会消耗太多内存吗?
有人可能会认为,在所有元组、字符串、列表等中使用 Py_ssize_t 会浪费空间。但这并非如此:在 32 位机器上,没有变化。在 64 位机器上,许多容器的大小不会改变,例如
- 在列表和元组中,指针紧跟在 ob_size 成员之后。这意味着编译器当前插入 4 个填充字节;通过更改,这些填充字节成为大小的一部分。
- 在字符串中,ob_shash 字段位于 ob_size 之后。此字段在大多数 64 位系统(Win64 除外)上是 long 类型,它是 64 位类型,因此编译器也会在其前面插入填充。
未解决的问题
- Marc-Andre Lemburg 评论说,应保留与现有源代码的完全向后兼容性。特别是,即使调用者传递 int*,具有 Py_ssize_t* 输出参数的函数也应继续正确运行。
目前尚不清楚可以使用什么策略来实现该要求。
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0353.rst