Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 将透明地返回一个长整型对象;PyInt_AsSsize_t 将透明地处理长整型对象。

引入了新的函数指针 typedef ssizeargfunc、ssizessizeargfunc、ssizeobjargproc、ssizessizeobjargproc 和 lenfunc。缓冲区接口函数类型现在称为 readbufferproc、writebufferproc、segcountproc 和 charbufferproc。

为 PyArg_ParseTuple Py_BuildValue、PyObject_CallFunction 和 PyObject_CallMethod 引入了新的转换代码 'n'。此代码在 Py_ssize_t 上操作。

如果定义了宏 PY_SSIZE_T_CLEAN 并在包含 Python.h 之前,转换代码 's#' 和 't#' 将输出 Py_ssize_t,如果未定义该宏,则继续输出 int。

在需要从 size_t/Py_ssize_t 转换为 int 的地方,转换策略是根据具体情况选择的(参见下一节)。

为了防止将假定 32 位大小类型的扩展模块加载到具有 64 位大小类型的解释器中,Py_InitModule4 已重命名为 Py_InitModule4_64。

转换指南

模块作者可以选择是否在他们的代码中支持此 PEP;如果他们支持,他们可以选择不同级别的兼容性。

如果模块未转换为支持此 PEP,它将在 32 位系统上继续 unmodified 工作。在 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 槽,可能需要额外的 typedef;或者,通过替换

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--)

如果索引从 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 之后。此字段的类型是 long,在大多数 64 位系统上(Win64 除外)是 64 位类型,因此编译器也会在其之前插入填充。

未解决的问题

  • Marc-Andre Lemburg 评论说,应该保留与现有源代码的完全向后兼容性。特别是,具有 Py_ssize_t* 输出参数的函数应该继续正确运行,即使调用者传递 int*。

    目前尚不清楚可以使用什么策略来实现该要求。


来源:https://github.com/python/peps/blob/main/peps/pep-0353.rst

最后修改:2025-02-01 08:59:27 GMT