Skip to content

7.3。多态调度

原文: http://numba.pydata.org/numba-doc/latest/developer/dispatching.html

使用 jit()vectorize() 编译的函数是开放式的:可以使用许多不同的输入类型调用它们,并且必须选择(可能在运行中编译)正确的低级专业化。我们在此解释如何实施这一机制。

7.3.1。要求

JIT 编译的函数可以采用多个参数,并且在选择特化时会考虑每个参数。因此,它是多调度的一种形式,比单一调度更复杂。

每个参数都根据其 Numba 类型进行选择。 Numba 类型通常比 Python 类型更精细:例如,Numba 根据其维度和布局(C-contiguous 等)对 Numpy 数组进行不同的类型。

一旦为每个参数推断出 Numba 类型,就必须在可用的参数中选择特化;或者,如果找不到合适的专业化,则必须编制新的专业化。这不是一个简单的决定:可以有多个与给定具体签名兼容的特化(例如,假设两个参数函数已经为(float64, float64)(complex64, complex64)编译了特化,并且它用(float32, float32)调用)。

因此,调度机制中有两个关键步骤:

  1. 推断 Numba 类型的具体论证
  2. 为推断的 Numba 类型选择最佳可用专业化(或选择编译新的专业化)

7.3.1.1。编译时与运行时

本文档讨论了在运行时完成的调度,即从纯 Python 调用 JIT 编译的函数时。在这种情况下,绩效很重要。为了保持 Python 中正常函数调用开销的范围,调度的开销应该保持在一微秒以下。当然,_ 越快越好 _ ......

当从另一个 JIT 编译的函数调用 JIT 编译的函数时(在 nopython 模式中),在编译时使用非性能关键机制解析多态,承载零运行时性能开销。

注意

在实践中,这里描述的性能关键部分用 C 编码。

7.3.2。类型分辨率

因此,第一步是在调用时推断出每个函数的具体参数的 Numba 类型。鉴于 Numba 类型与 Python 类型相比更精细的粒度,人们不能简单地查找对象的类并用它键入字典以获得相应的 Numba 类型。

相反,有一种机制来检查对象,并根据其 Python 类型查询各种属性以推断出适当的 Numba 类型。这可能或多或少复杂:例如,Python int参数将始终推断为 Numba intp(指针大小的整数),但 Python tuple参数可以推断出多个 Numba 类型(取决于元组的大小和每个元素的具体类型)。

Numba 类型系统是高级的,用纯 Python 编写;有一个基于泛型函数的纯 Python 机制来做推理(在numba.typing.typeof中)。该机制用于编译时推理,例如,关于常数。不幸的是,它对于基于运行时的基于值的调度来说太慢了。它仅用作很少使用(或难以推断)类型的后备,并且表现出多微秒的开销。

7.3.2.1。类型代码

Numba 类型系统实际上太高级别,无法从 C 代码中有效地操作。因此,C 调度层使用基于整数类型代码的另一种表示。每个 Numba 类型在构造时都会获得唯一的整数类型代码;此外,实习系统确保不会创建两个相同类型的实例。因此,调度层能够 _ 通过使用简单的整数类型代码来避开 _ Numba 类型系统的开销,这些类型可以适用于众所周知的优化(快速哈希表等)。

类型解析步骤的目标变为:为每个函数的具体参数推断 Numba _ 类型代码 _。理想情况下,它不再处理 Numba 类型......

7.3.2.2。硬编码快速路径

在避开类型系统的抽象和面向对象开销的同时,整数类型代码仍然具有相同的概念复杂性。因此,加速推理的一个重要技术是首先检查最重要的类型,并为每个类型硬编码快速解决方案。

有几种类型可以从这种优化中受益,特别是:

  • 基本的 Python 标量(boolintfloatcomplex);
  • 基本的 Numpy 标量(各种整数,浮点数,复数);
  • 具有某些维度和基本元素类型的 Numpy 数组。

在几次简单检查之后,每个快速路径理想地使用硬编码结果值或直接表查找。

但是,我们不能将该技术应用于所有参数类型;将会出现临时内部缓存的爆炸性增长,并且难以维护。此外,硬编码快速路径的递归应用不一定会组合成低开销(例如,在嵌套元组的情况下)。

7.3.2.3。基于指纹的类型代码缓存

对于非平凡类型(例如,想象一个元组或 Numpy datetime64数组),硬编码的快速路径不匹配。然后另一个机制开始,更通用。

这里的原则是检查每个参数值,就像纯 Python 机制一样,并明确地描述它的 Numba 类型。区别在于 _ 我们实际上并不计算 Numba 类型 。相反,我们计算一个简单的字节串,这是 Numba 类型的低级可能表示: 指纹 _。指纹格式设计得很短,从 C 代码计算起来非常简单(实际上,它具有类似字节码的格式)。

一旦计算了指纹,就会在高速缓存中将指纹映射到 typecodes。缓存是一个哈希表,由于指纹通常非常短(很少超过 20 个字节),因此查找速度很快。

如果缓存查找失败,则必须首先使用慢速纯 Python 机制计算类型代码。幸运的是,这只会发生一次:在后续调用中,将为给定的指纹返回缓存的类型代码。

在极少数情况下,无法有效计算指纹。某些类型的情况就是这种情况,这些类型无法从 C 中轻松检查:例如cffi函数指针。然后,在每个函数调用中使用这样的参数调用缓慢的 Pure Python 机制。

注意

两个指纹可以表示单个 Numba 类型。这不会使机制不正确;它只会创建更多的缓存条目。

7.3.2.4。摘要

函数参数的类型解析按顺序包含以下机制:

  • 尝试一些硬编码的快速路径,用于常见的简单类型。
  • 如果上述操作失败,请计算参数的指纹并在缓存中查找其类型代码。
  • 如果上述所有方法都失败了,请调用纯 Python 机制,它将为参数确定 Numba 类型(并查找其类型代码)。

7.3.3。专业化选择

在上一步中,已为 JIT 编译函数的每个具体参数确定了整数类型代码。现在,仍然要将该具体签名与该函数的每个可用特化项进行匹配。可以有三种结果:

  • 有一个令人满意的最佳匹配:然后调用相应的特化(它将处理参数拆箱和其他细节)。
  • 两个或更多“最佳匹配”之间存在联系:引发异常,拒绝解决歧义。
  • 没有令人满意的匹配:为推断出的具体参数类型编制了新的专业化。

选择通过循环所有可用的特化,并计算每个具体参数类型与特化的预期签名中的相应类型的兼容性来工作。具体来说,我们感兴趣的是:

  1. 是否允许具体参数类型隐式转换为特化的参数类型;
  2. 如果是这样,转换的语义(用户可见)成本。

7.3.3.1。隐式转换规则

从源类型到目标类型有五种可能的隐式转换(注意这是一种非对称关系):

  1. _ 完全匹配 _:两种类型相同;这是理想的情况,因为专业化的行为与预期完全一致;
  2. _ 同类促销 _:两种类型属于同一“种类”(例如int32int64是两种整数类型),源类型可以无损转换为目标类型(例如从int32int64,但不是相反的);
  3. _ 安全转换 _:这两种类型属于不同种类,但源类型可以合理地转换为目标类型(例如从int32float64,但不能反向);
  4. _ 不安全转换 _:从源类型到目标类型可以进行转换,但可能会失去精度,幅度或其他所需的质量。
  5. _ 无转换 _:没有正确或合理有效的方法在两种类型之间进行转换(例如在int64datetime64之间,或者在 C 连续数组和 Fortran 连续数组之间) 。

当检查专门化时,后两种情况将其从最终选择中消除:即,当至少一个参数具有 _ 无转换 _ 或仅 _ 不安全转换 _ 到签名的参数类型时。

注意

但是,如果在 jit() 调用中使用显式签名编译函数(因此不允许编译新的特化),则允许 _ 不安全转换 _。

7.3.3.2。候选人和最佳匹配

如果上述规则没有消除专业化,它将进入 _ 候选人 _ 列表以供最终选择。这些候选者按有序的 4-uple 整数排序:(number of unsafe conversions, number of safe conversions, number of same-kind promotions, number of exact matches)(注意元组元素的总和等于参数的数量)。最佳匹配是按升序排序的第一个结果,因此更喜欢完全匹配促销,促销而不是安全转化,安全转换不安全转化。

7.3.3.3。实施

上述机制适用于整数类型代码,而不适用于 Numba 类型。它使用内部哈希表存储每对兼容类型的可能转换类型。内部哈希表部分在启动时构建(对于内置普通类型,如int32int64等),部分动态填充(对于任意复杂类型,如数组类型:例如,允许使用 C 连续的 2D 数组,其中函数需要非连续的 2D 数组。

7.3.3.4。摘要

选择正确的专业化涉及以下步骤:

  • 检查每个可用的特化并将其与具体参数类型进行匹配。
  • 消除任何至少有一个参数不能提供足够兼容性的专业化。
  • 如果有剩余的候选者,请在保留类型的语义方面选择最佳的候选者。

7.3.4。杂项

调度性能的一些基准存在于 Numba 基准测试存储库中。

有关机械特定方面的一些单元测试可在numba.tests.test_typeinfernumba.tests.test_typeof中找到。更高级别的调度测试在numba.tests.test_dispatcher中。



回到顶部