Cuteufo
Cuteufo

Reputation: 575

cython: a function has various type of parameter

I am trying to modify an old Python code into Cython to improve speed. A class has a init function allowing various types of parameter:

class dRect:
    def __init__(self, *args):
        if len(args) == 1: # actual input is a list of four float numbers
            self.x0, self.y0, self.x1, self.y1 = args[0]
        elif len(args) == 4:  # actual input is four float numbers
            self.x0, self.y0, self.x1, self.y1 = args

# sample of initiating dRect instance
a = dRect([0, 1, 2, 3])
b = dRect(0, 1, 2, 3)

What is the proper way to modify it into a Cython code? Or which part of the documentation should I dive into? Thanks.

Upvotes: 1

Views: 176

Answers (1)

Golden Rockefeller
Golden Rockefeller

Reputation: 313

The amount of speed up (in any) depends on what you are allow to do (up to 10x boost in my case).

  1. Cdef your class. A cdefed class will be faster than a non-cdef class (see MyCyClass vs MyPyClass in tests below)
  2. Prefer passing Python floats over passing Python integers in the constructor for the cdefed class. If you pass in Python floats instead of Python integers, the constructor is faster. Probably because converting python floats to double is faster than converting python integer to double.
  3. Do fast instantiation using __new__() in a free cdef function and avoid initialization with tuples if you can avoid it.

Test:


cdef class MyCyClass:
    cdef public double x0
    cdef public double x1
    cdef public double x2
    cdef public double x3
    
    def __init__(self, *args):
        self.x0 = args[0]
        self.x1 = args[1]
        self.x2 = args[2]
        self.x3 = args[3]
    
class MyPyClass:
    def __init__(self, *args):
        self.x0 = args[0]
        self.x1 = args[1]
        self.x2 = args[2]
        self.x3 = args[3]

cdef new_MyCyClass_from_seq(args):
    my_obj = MyCyClass.__new__(MyCyClass)
    init_MyCyClass_from_seq(my_obj, args)

cdef init_MyCyClass_from_seq(MyCyClass my_obj, args):
    my_obj.x0 = args[0]
    my_obj.x1 = args[1]
    my_obj.x2 = args[2]
    my_obj.x3 = args[3]
    
cdef new_MyCyClass_from_double(double x0, double x1, double x2,  double x3):
    my_obj = MyCyClass.__new__(MyCyClass)
    init_MyCyClass_from_double(my_obj, x0, x1, x2, x3)
    
cdef init_MyCyClass_from_double(MyCyClass my_obj, double x0, double x1, double x2,  double x3):
    my_obj.x0 = x0
    my_obj.x1 = x1
    my_obj.x2 = x2
    my_obj.x3 = x3

def timefunc(name):
    def timedecorator(f):
        cdef Py_ssize_t L, i

        print("Running", name)
        for L in [1, 10, 100, 1000, 10000]:
            start = time.perf_counter() 
            f(L)
            end = time.perf_counter()
            print(format((end-start) / loops * 1e6, "2f"), end=" ")
            sys.stdout.flush()

        print("μs")
    return timedecorator

print()
print("INITIALISATIONS")
cdef Py_ssize_t loops = 100000

@timefunc("use constructor of non-cdef class with type(input) = float")
def _(Py_ssize_t L):
    cdef Py_ssize_t i
    
    my_obj = MyPyClass(1., 2., 3., 4.)
    
    for i in range(loops):
        my_obj = MyPyClass(1., 2., 3., 4.)
        
    return my_obj
    
    
@timefunc("use constructor (cdef) with type(input) = float")
def _(Py_ssize_t L):
    cdef MyCyClass my_obj = MyCyClass(1., 2., 3., 4.)
    cdef Py_ssize_t i
    
    for i in range(loops):
        # Notice the decimal place to denote 
        my_obj = MyCyClass(1., 2., 3., 4.)
        
    return my_obj
    
@timefunc("use constructor (cdef) with type(input) = int")
def _(Py_ssize_t L):
    cdef MyCyClass my_obj = MyCyClass(1, 2, 3, 4)
    cdef Py_ssize_t i
    
    for i in range(loops):
        my_obj = MyCyClass(1, 2, 3, 4)
        
    return my_obj
    
    
@timefunc("use new (cdef) with double init, input is double")
def _(Py_ssize_t L):
    cdef MyCyClass my_obj = MyCyClass(1, 2, 3, 4)
    cdef Py_ssize_t i
    
    for i in range(loops):
        my_obj = new_MyCyClass_from_double(1., 2., 3., 4.)
        
    return my_obj    
    
    
@timefunc("use new (cdef) with double init, input is int")
def _(Py_ssize_t L):
    cdef MyCyClass my_obj = MyCyClass(1, 2, 3, 4)
    cdef Py_ssize_t i
    
    for i in range(loops):
        new_MyCyClass_from_double(1, 2, 3, 4)
        
    return my_obj    
    
@timefunc("use new (cdef) and tuple init")
def _(Py_ssize_t L):
    cdef MyCyClass my_obj = MyCyClass(1., 2., 3., 4.)
    cdef Py_ssize_t i
    
    for i in range(loops):
        my_obj = new_MyCyClass_from_seq((1., 2., 3., 4.))
        
    return my_obj

Here are my timings

Running use constructor of non-cdef class with type(input) = float

  • 0.194803 0.199249 0.196051 0.201220 0.265174 μs (slowest)

Running use constructor (cdef) with type(input) = float

  • 0.044000 0.048895 0.044641 0.045356 0.048148 μs

Running use constructor (cdef) with type(input) = int

  • 0.090032 0.169924 0.089831 0.090147 0.091007 μs

Running use new (cdef) with double init, input is double

  • 0.026093 0.025533 0.029145 0.023639 0.024517 μs (fastest)

Running use new (cdef) with double init, input is int

  • 0.025054 0.024114 0.024084 0.025460 0.024183 μs (fastest, equivalent to above)

Running use new (cdef) and tuple init

  • 0.042559 0.042985 0.042348 0.043527 0.043123 μs

Upvotes: 2

Related Questions