NBEP 3: JIT Classes
- Author
Siu Kwan Lam
- Date
Dec 2015
- Status
Draft
Introduction
Numba does not yet support user-defined classes. Classes provide useful abstraction and promote modularity when used right. In the simplest sense, a class specifies the set of data and operations as attributes and methods, respectively. A class instance is an instantiation of that class. This proposal will focus on supporting this simple usecase of classes–with just attributes and methods. Other features, such as class methods, static methods, and inheritance are deferred to another proposal, but we believe these features can be easily implemented given the foundation described here.
Proposal: jit-classes
A JIT-classes is more restricted than a Python class. We will focus on the following operations on a class and its instance:
Instantiation: create an instance of a class using the class object as the constructor:
cls(*args, **kwargs)
Destruction: remove resources allocated during instantiation and release all references to other objects.
Attribute access: loading and storing attributes using
instance.attr
syntax.Method access: loading methods using
instance.method
syntax.
With these operations, a class object (not the instance) does not need to be materialize. Using the class object as a constructor is fully resolved (a runtime implementation is picked) during the typing phase in the compiler. This means a class object will not be first class. On the other hand, implementing a first-class class object will require an “interface” type, or the type of class.
The instantiation of a class will allocate resources for storing the data attributes. This is described in the “Storage model” section. Methods are never stored in the instance. They are information attached to the class. Since a class object only exists in the type domain, the methods will also be fully resolved at the typing phase. Again, numba do not have first-class function value and each function type maps uniquely to each function implementation (this needs to be changed to support function value as argument).
A class instance can contain other NRT reference-counted object as attributes. To properly clean up an instance, a destructor is called when the reference count of the instance is dropped to zero. This is described in the “Reference count and descructor” section.
Storage model
For compatibility with C, attributes are stored in a simple plain-old-data structure. Each attribute are stored in a user-defined order in a padded (for proper alignment), contiguous memory region. An instance that contains three fields of int32, float32, complex64 will be compatible with the following C structure:
struct {
int32 field0;
float32 field1;
complex64 field2;
};
This will also be compatible with an aligned NumPy structured dtype.
Methods
Methods are regular function that can be bounded to an instance.
They can be compiled as regular function by numba.
The operation getattr(instance, name)
(getting an attribute name
from
instance
) binds the instance to the requested method at runtime.
The special __init__
method is also handled like regular functions.
__del__
is not supported at this time.
Reference count and destructor
An instance of jit-class is reference-counted by NRT. Since it may contain other NRT tracked object, it must call a destructor when its reference count dropped to zero. The destructor will decrement the reference count of all attributes by one.
At this time, there is no support for user defined __del__
method.
Proper cleanup for cyclic reference is not handled at this time. Cycles will cause memory leak.
Type inference
So far we have not described the type of the attributes or the methods. Type information is necessary to materailize the instance (e.g. allocate the storage). The simplest way is to let user provide the type of each attributes as well as the ordering; for instance:
dct = OrderedDict()
dct['x'] = int32
dct['y'] = float32
Allowing user to supply an ordered dictionary will provide the name, ordering and types of the attributes. However, this statically typed semantic is not as flexible as the Python semantic which behaves like a generic class.
Inferring the type of attributes is difficult. In a previous attempt to
implement JIT classes, the __init__
method is specialized to capture
the type stored into the attributes. Since the method can contain arbitrary
logic, the problem can become a dependent typing problem if types are assigned
conditionally depending on the value. (Very few languages implement dependent
typing and those that does are mostly theorem provers.)
Example: typing function using an 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
Example: typing function using a list of 2-tuples
spec = [('x', numba.int32),
('y', numba.float32)]
@jitclass(spec)
class Vec(object):
...
Creating multiple jitclasses from a single class object
The jitclass(spec) decorator creates a new jitclass type even when applied to the same class object and the same type specification.
class Vec(object):
...
Vec1 = jitclass(spec)(Vec)
Vec2 = jitclass(spec)(Vec)
# Vec1 and Vec2 are two different jitclass types
Usage from the Interpreter
When constructing a new instance of a jitclass, a “box” is created that wraps the underlying jitclass instance from numba. Attributes and methods are accessible from the interpreter. The actual implementation will be in numba compiled code. Any Python object is converted to its native representation for consumption in numba. Similarly, the returned value is converted to its Python representation. As a result, there may be overhead in manipulating jitclass instances in the interpreter. This overhead is minimal and should be easily amortized by more efficient computation in the compiled methods.
Support for property, staticmethod and classmethod
The use of property
is accepted for getter and setter only. Deleter is not
supported.
The use of staticmethod
is not supported.
The use of classmethod
is not supported.
Inheritance
Class inheritance is not considered in this proposal. The only accepted base class for a jitclass is object.
Supported targets
Only the CPU target (including the parallel target) is supported. GPUs (e.g. CUDA and HSA) targets are supported via an immutable version of the jitclass instance, which will be described in a separate NBEP.
Other properties
Given:
spec = [('x', numba.int32),
('y', numba.float32)]
@jitclass(spec)
class Vec(object):
...
isinstance(Vec(1, 2), Vec)
is True.type(Vec(1, 2))
may not beVec
.
Future enhancements
This proposal has only described the basic semantic and functionality of a jitclass. Additional features will be described in future enhancement proposals.