NBEP 1: 整数类型变化

作者:

安托万·皮特鲁

日期:

2015年7月

状态:

最终

当前语义

Numba 中整数类型的推断目前有一些微妙之处和一些特殊情况。简单的情况是当某个变量具有明显的 Numba 类型时(例如因为它是由 Numpy 标量类型的构造函数调用结果,如 np.int64)。这种情况不存在模糊性。

较不简单的情况是当一个变量不带有这种显式信息时。这可能是因为它是从Python内置的``int``值推断出来的,或者是从两个整数之间的算术运算中推断出来的,或者其他情况。然后,Numba有一系列的规则来推断结果的Numba类型,特别是它的符号和位宽。

目前,一般情况可以概括为:从小开始,根据需要逐渐扩大。具体来说:

  1. 每个常量或伪常量都是使用 最小的有符号整数类型 推断出来的,该类型可以正确表示它(或者,对于 2**632**64 - 1 之间的正整数,可能是 uint64)。

  2. 操作的结果会被类型化,以确保在面对溢出和其他量级增加时(例如,int32 + int32 会被类型化为 int64)的安全表示。

  3. 作为一种例外,用作函数参数的 Python int 总是被类型化为 intp,即指针大小的整数。这是为了避免编译的特殊化泛滥,因为否则输入参数中的各种整数位宽可能会产生多个签名。

备注

上述第二条规则(“尊重量级增加”规则)再现了 Numpy 在标量值算术运算中的行为。然而,Numba 与 Numpy 标量相比,具有不同的实现和性能约束。

顺便提一下,值得注意的是,Numpy 数组并没有实现这一规则(即 array(int32) + array(int32) 的类型是 array(int32),而不是 array(int64))。可能是因为这样性能更可控。

这有几个不明显的副作用:

  1. 在函数内经过多次操作后,很难预测一个值的确切类型。例如,表达式树中的基本操作数可能是 int8,但最终结果可能是 int64。这是否可取是一个开放的问题;它有利于正确性,但可能对性能不利。

  2. 在尝试遵循正确性优于可预测性规则时,某些值实际上可以离开整数领域。例如,int64 + uint64 被类型化为 float64,以避免幅度损失(但顺便说一句,这会在大整数值上失去精度…),再次遵循 Numpy 对标量的语义。这通常不是用户所期望的。

  3. 更复杂的场景可能会在类型统一阶段产生意外错误。一个例子在 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 类型统一系统的人可以理解其中的原因。但用户却陷入了谜团。

提案:可预测的宽度保持输入

我们提议彻底改变当前的打字哲学。与其采用“从小开始,按需增长”,我们提议“从大开始,保持宽度不变”。

具体来说:

  1. 作为函数参数使用的Python int 值的类型不会改变,因为它工作得令人满意,并且不会让用户感到意外。

  2. 整数 *常量*(和伪常量)的类型会根据整数参数的类型进行匹配。也就是说,每个非显式类型的整数常量都被类型化为 intp,即指针大小的整数;除了在极少数情况下需要 int64``(在32位系统上)或 ``uint64

  3. 对整数的操作会提升位宽到 intp,如果更小的话,否则不会提升。例如,在32位机器上,int8 + int8 的类型是 int32int32 + int32 也是如此。然而,int64 + int64 的类型是 int64

  4. 此外,有符号和无符号之间的混合操作会回退到有符号类型,同时遵循相同的位宽规则。例如,在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 中,用户可以假设任意精度的整数而不会遇到任何截断问题。