NBEP 3: JIT 类

作者

Siu Kwan Lam

日期

2015 年 12 月

状态

草稿

简介

Numba 尚未支持用户定义的类。类在正确使用时可提供有用的抽象并提升模块化。最简单地说,类分别将数据和操作集合指定为属性和方法。类实例是该类的一个实例化。本提案将侧重于支持类的这种简单用例——仅包含属性和方法。其他功能,例如类方法、静态方法和继承,将推迟到另一个提案中讨论,但我们相信基于此处描述的基础,这些功能可以轻松实现。

提案: jit-类

JIT 类比 Python 类受到更多限制。我们将重点关注类及其实例的以下操作:

  • 实例化:使用类对象作为构造函数创建类实例:cls(*args, **kwargs)

  • 销毁:移除实例化期间分配的资源,并释放对其他对象的所有引用。

  • 属性访问:使用 instance.attr 语法加载和存储属性。

  • 方法访问:使用 instance.method 语法加载方法。

通过这些操作,类对象(而非实例)无需具体化。在编译器的类型推断阶段,将类对象用作构造函数的操作会得到完全解析(选择一个运行时实现)。这意味着类对象将不是一级对象(first class)。另一方面,实现一级类对象将需要一个“接口”类型,或者说是类的类型。

类的实例化将分配资源用于存储数据属性。这在“存储模型”部分中进行了描述。方法永远不会存储在实例中。它们是附加到类的信息。由于类对象只存在于类型域中,因此方法也将在类型推断阶段完全解析。同样,Numba 没有一级函数值,并且每种函数类型都唯一映射到每个函数实现(这需要更改以支持函数值作为参数)。

类实例可以包含其他 NRT 引用计数的对象作为属性。为了正确清理实例,当实例的引用计数降至零时,会调用析构函数。这在“引用计数和析构函数”部分中进行了描述。

存储模型

为了与 C 兼容,属性存储在简单的“纯旧数据结构”(plain-old-data structure)中。每个属性都以用户定义的顺序存储在经过填充(以确保正确对齐)的连续内存区域中。包含 int32、float32、complex64 三个字段的实例将与以下 C 结构兼容:

struct {
    int32     field0;
    float32   field1;
    complex64 field2;
};

这也将与对齐的 NumPy 结构化数据类型兼容。

方法

方法是可以绑定到实例的常规函数。它们可以由 Numba 编译为常规函数。操作 getattr(instance, name)(从 instance 获取属性 name)在运行时将实例绑定到所请求的方法。

特殊的 __init__ 方法也像常规函数一样处理。

目前不支持 __del__

引用计数和析构函数

JIT 类的实例由 NRT 进行引用计数。由于它可能包含其他 NRT 跟踪的对象,因此当其引用计数降至零时,必须调用析构函数。析构函数将所有属性的引用计数减一。

目前不支持用户定义的 __del__ 方法。

目前未处理循环引用的正确清理。循环将导致内存泄漏。

类型推断

到目前为止,我们尚未描述属性或方法的类型。类型信息对于实例化(例如分配存储)是必要的。最简单的方法是让用户提供每个属性的类型以及顺序;例如

dct = OrderedDict()
dct['x'] = int32
dct['y'] = float32

允许用户提供有序字典将提供属性的名称、顺序和类型。然而,这种静态类型语义不如行为类似于泛型类的 Python 语义灵活。

推断属性类型很困难。在之前实现 JIT 类的尝试中,__init__ 方法被专门化以捕获存储在属性中的类型。由于方法可以包含任意逻辑,如果类型根据值有条件地分配,问题可能会变成一个依赖类型问题。(很少有语言实现依赖类型,而那些实现的语言大多是定理证明器。)

示例:使用 OrderedDict 的类型推断函数

spec = OrderedDict()
spec['x'] = numba.int32
spec['y'] = numba.float32

@jitclass(spec)
class Vec(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def add(self, dx, dy):
        self.x += dx
        self.y += dy

示例:使用 2 元组列表的类型推断函数

spec = [('x', numba.int32),
        ('y', numba.float32)]

@jitclass(spec)
class Vec(object):
    ...

从单个类对象创建多个 jit 类

jitclass(spec) 装饰器即使应用于相同的类对象和相同的类型规范,也会创建一个新的 jit 类类型。

class Vec(object):
  ...

Vec1 = jitclass(spec)(Vec)
Vec2 = jitclass(spec)(Vec)
# Vec1 and Vec2 are two different jitclass types

在解释器中的使用

当构造一个新的 jit 类实例时,会创建一个“箱”(box)来包装 Numba 中的底层 jit 类实例。属性和方法可以从解释器中访问。实际实现将在 Numba 编译代码中。任何 Python 对象都会转换为其原生表示形式以供 Numba 使用。同样,返回值也会转换为其 Python 表示形式。因此,在解释器中操作 jit 类实例可能会产生开销。这种开销是最小的,并且应该通过编译方法中更高效的计算轻松摊销。

支持 property、staticmethod 和 classmethod

property 的使用仅支持 getter 和 setter。不支持 deleter。

不支持 staticmethod 的使用。

不支持 classmethod 的使用。

继承

本提案中不考虑类继承。jit 类唯一接受的基类是 object

支持的目标

目前仅支持 CPU 目标(包括并行目标)。GPU(例如 CUDA 和 HSA)目标通过 jit 类实例的不可变版本提供支持,这将在单独的 NBEP 中描述。

其他属性

给定

spec = [('x', numba.int32),
        ('y', numba.float32)]

@jitclass(spec)
class Vec(object):
    ...
  • isinstance(Vec(1, 2), Vec) 为 True。

  • type(Vec(1, 2)) 可能不是 Vec

未来增强

本提案仅描述了 jit 类的基本语义和功能。更多特性将在未来的增强提案中描述。