NBEP 1: 整数类型变化
- 作者:
安托万·皮特鲁
- 日期:
2015年7月
- 状态:
最终
当前语义
Numba 中整数类型的推断目前有一些微妙之处和一些特殊情况。简单的情况是当某个变量具有明显的 Numba 类型时(例如因为它是由 Numpy 标量类型的构造函数调用结果,如 np.int64
)。这种情况不存在模糊性。
较不简单的情况是当一个变量不带有这种显式信息时。这可能是因为它是从Python内置的``int``值推断出来的,或者是从两个整数之间的算术运算中推断出来的,或者其他情况。然后,Numba有一系列的规则来推断结果的Numba类型,特别是它的符号和位宽。
目前,一般情况可以概括为:从小开始,根据需要逐渐扩大。具体来说:
每个常量或伪常量都是使用 最小的有符号整数类型 推断出来的,该类型可以正确表示它(或者,对于
2**63
到2**64 - 1
之间的正整数,可能是uint64
)。操作的结果会被类型化,以确保在面对溢出和其他量级增加时(例如,
int32 + int32
会被类型化为int64
)的安全表示。作为一种例外,用作函数参数的 Python
int
总是被类型化为intp
,即指针大小的整数。这是为了避免编译的特殊化泛滥,因为否则输入参数中的各种整数位宽可能会产生多个签名。
备注
上述第二条规则(“尊重量级增加”规则)再现了 Numpy 在标量值算术运算中的行为。然而,Numba 与 Numpy 标量相比,具有不同的实现和性能约束。
顺便提一下,值得注意的是,Numpy 数组并没有实现这一规则(即 array(int32) + array(int32)
的类型是 array(int32)
,而不是 array(int64)
)。可能是因为这样性能更可控。
这有几个不明显的副作用:
在函数内经过多次操作后,很难预测一个值的确切类型。例如,表达式树中的基本操作数可能是
int8
,但最终结果可能是int64
。这是否可取是一个开放的问题;它有利于正确性,但可能对性能不利。在尝试遵循正确性优于可预测性规则时,某些值实际上可以离开整数领域。例如,
int64 + uint64
被类型化为float64
,以避免幅度损失(但顺便说一句,这会在大整数值上失去精度…),再次遵循 Numpy 对标量的语义。这通常不是用户所期望的。更复杂的场景可能会在类型统一阶段产生意外错误。一个例子在 Github issue 1299,其要点在这里重现:
@jit(nopython=True) def f(): variable = 0 for i in range(1): variable = variable + 1 return np.arange(variable)
在撰写本文时,这在64位系统上编译失败,出现以下错误:
numba.errors.TypingError: Failed at nopython (nopython frontend) Can't unify types of variable '$48.4': $48.4 := {array(int32, 1d, C), array(int64, 1d, C)}
熟悉 Numba 类型统一系统的人可以理解其中的原因。但用户却陷入了谜团。
提案:可预测的宽度保持输入
我们提议彻底改变当前的打字哲学。与其采用“从小开始,按需增长”,我们提议“从大开始,保持宽度不变”。
具体来说:
作为函数参数使用的Python
int
值的类型不会改变,因为它工作得令人满意,并且不会让用户感到意外。整数 *常量*(和伪常量)的类型会根据整数参数的类型进行匹配。也就是说,每个非显式类型的整数常量都被类型化为
intp
,即指针大小的整数;除了在极少数情况下需要int64``(在32位系统上)或 ``uint64
。对整数的操作会提升位宽到
intp
,如果更小的话,否则不会提升。例如,在32位机器上,int8 + int8
的类型是int32
,int32 + int32
也是如此。然而,int64 + int64
的类型是int64
。此外,有符号和无符号之间的混合操作会回退到有符号类型,同时遵循相同的位宽规则。例如,在32位机器上,
int8 + uint16
的类型为int32
,同样uint32 + int32
也是int32
。
提案影响
语义
通过这个提议,语义变得更加清晰。无论函数的参数和常量是否显式类型化,函数中任何点的各种表达式的结果类型都易于预测。
当使用内置的 Python int
时,用户会得到可接受的范围(32 或 64 位,取决于系统的位数),并且在所有计算中类型保持不变。
当显式使用较小的位宽时,中间结果不会遭受幅度损失,因为它们的位宽被提升为 intp
。
如上所示,类型统一系统引发烦恼的可能性也较小。用户必须强制几种不同类型才能遇到这样的错误。
一个值得关注的问题是与Numpy的标量语义不一致;但与此同时,这也使得Numba的标量语义更接近数组语义(无论是Numba的还是Numpy的),这似乎也是一个理想的结果。
值得一提的是,一些整数来源,例如内置的 range()
函数,总是产生32位整数或更大的整数。这个提议可能是一个将它们标准化为 intp
的机会。
性能
除了在简单的情况下,当前整数常量的“最佳匹配”行为似乎不太可能真正带来性能优势。毕竟,Numba代码中的大多数整数要么存储在数组中(具有用户选择的已知类型),要么用作索引,其中 int8
不太可能比 intp
表现更好(实际上,如果LLVM不能优化掉所需的符号扩展,它可能会更差)。
顺便提一下,默认使用 intp
而不是 int64
确保了32位系统不会遭受算术性能不佳的问题。
实现
乐观地看,这个提议可能会稍微简化一些 Numba 的内部机制。或者,至少,它不会威胁到使它们显著复杂化。
限制
这个提案并没有真正解决有符号和无符号整数的组合问题。它主要针对解决位宽问题,这是用户经常遇到的一个痛点。在Numba编译的代码中,无符号整数实际上非常少见,除非明确要求,因此它们带来的问题要少得多。
在位宽方面,32位系统仍可能根据常量值显示差异:如果常量太大而无法适应32位,则其类型为 int64
,这会影响其他计算。这将是当前行为的回忆,但更为罕见且控制得更好。
长期视野
虽然我们认为这个提案使 Numba 的行为更加规范和可预测,但它也使其与纯 Python 语义的通用兼容性更远,在纯 Python 中,用户可以假设任意精度的整数而不会遇到任何截断问题。