Arraybuffer: fast buffer for typed memoryviews

There is a problem with fast creation of cython.array and as consequence — slow creation of memoryviews. Backward compatibility issue dosn't allow to resolve it. So there is an attempt to create a class arraybuffer.

The arraybuffer object is a buffer for multimensional arrays and typed memoryviews. Creation of arraybuffer object bases on simple description of array.

In [1]:
from __future__ import print_function, unicode_literals
from arraybuffer import arraybuffer, arraybuffer_dsc
import gc

%load_ext cython

First we should create the description of arraybuffer. Then we create arraybuffer object:

In [2]:
dsc = arraybuffer_dsc(shape=(10,), format='d', order='C')
ab = arraybuffer(dsc)
print(dsc)
print(ab)
<arraybuffer_dsc ndim=1 itemsize=8 shape=(10,) format="b'd'" order="C">
<arraybuffer ndim=1 itemsize=8 shape=(10,) format="b'd'" len=80 order="C">

It's similar to a way of creation of cpython.array objects: first — template, then — object:

In [3]:
%%cython

from cpython.array cimport array, clone

a_tmp = array('d')
a = clone(a_tmp, 10, 1)
print(a)
array('d', [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])

This way of creation of arraybuffer makes it as fast as cpython.array.

In [4]:
%%cython

#cython: language_level=3

from cython.view cimport array as cvarray
from arraybuffer cimport arraybuffer_dsc, arraybuffer
from cpython.array cimport array, clone

import numpy as np
cimport numpy as np

from libc.stdlib cimport malloc, free
from libc.time cimport clock

#import time
import gc

cdef long N = 1000

f8 = np.dtype('f8')
Ls = [1, 5, 10, 15, 30, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000]

def test_cvarray():
    cdef list ts = []
    cdef list _ts
    cdef int i,j
    cdef long t1, t2
    for L in Ls:
        _ts = []
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = cvarray((L,), sizeof(double), 'd')
            str(arr[0])
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts

def test_arraybuffer():
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        ap = arraybuffer_dsc((L,), 'd')
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = arraybuffer(ap)
            str(arr.shape)
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts

def test_cparray():
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        ac = array('d', [])
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = clone(ac, L, 0)
            str(arr[0])
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
    
def test_numpy():
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        np_tpl = np.empty((L,), dtype=f8)
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = np.empty_like(np_tpl)
            str(arr[0])
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
   
def test_calloc():
    cdef double *arr
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = <double*>malloc(sizeof(double*) * L)
                free(arr)
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
In [5]:
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')

import pandas as pd
pd.options.display.float_format = '{:.3f}'.format
from IPython.display import display

plt.figure(figsize=(12,6))
ts_arraybuffer = test_arraybuffer()
ts_cparray = test_cparray()
ts_cvarray = test_cvarray()
ts_numpy = test_numpy()
ts_calloc = test_calloc()

df = pd.DataFrame(
    {
        'L' : Ls,
        'calloc': ts_calloc,
        'arraybuffer': ts_arraybuffer,
        'cparray': ts_cparray,
        'cvarray': ts_cvarray,
        'numpy': ts_numpy,
    }, 
    columns = ['L', 'calloc', 'arraybuffer', 'cparray', 'cvarray', 'numpy'],
)
df = df.set_index('L')
display(df.transpose())

plt.plot(Ls, ts_calloc, marker='o', label='calloc', markersize=10)
plt.plot(Ls, ts_cparray, marker='o', label='cparray', markersize=10)
plt.plot(Ls, ts_arraybuffer, marker='o', label='arraybuffer', markersize=10)
plt.plot(Ls, ts_cvarray, marker='o', label='cvarray', markersize=10)
plt.plot(Ls, ts_numpy, marker='o', label='numpy', markersize=10)
plt.semilogx()
plt.grid()
plt.xlabel('L')
plt.ylabel('time')
plt.legend(loc='best')
plt.show()
L 1 5 10 15 30 50 75 100 250 500 750 1000 2500 5000 7500 10000
calloc 0.033 0.033 0.033 0.033 0.033 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067
arraybuffer 0.200 0.233 0.367 0.200 0.233 0.300 0.300 0.267 0.233 0.200 0.200 0.200 0.200 0.233 0.200 0.200
cparray 0.267 0.233 0.233 0.233 0.267 0.333 0.333 0.267 0.233 0.200 0.233 0.200 0.233 0.200 0.233 0.133
cvarray 0.633 0.600 1.033 0.667 0.667 0.633 0.633 0.633 0.533 0.533 0.667 0.567 0.533 0.567 0.433 0.433
numpy 0.333 0.333 0.367 0.367 0.333 0.333 0.333 0.333 0.400 0.400 0.400 0.400 0.400 0.400 0.400 0.400

Creation of 2-d arrays

In [6]:
%%cython

#cython: language_level=3

from cython.view cimport array as cvarray
from arraybuffer.arraybuffer cimport arraybuffer_dsc, arraybuffer
from cpython.array cimport array, clone

import numpy as np
cimport numpy as np

from libc.stdlib cimport malloc, free
from libc.time cimport clock

#import time
import gc

cdef long N = 100

f8 = np.dtype('f8')
Ls = [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

def test_cvarray():
    cdef list ts = []
    cdef list _ts
    cdef int i,j
    cdef long t1, t2
    for L in Ls:
        _ts = []
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = cvarray((L,L), sizeof(double), 'd')
            str(arr[0])
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts

def test_arraybuffer():
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        ap = arraybuffer_dsc((L,L), 'd')
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = arraybuffer(ap)
            str(arr.shape)
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
    
def test_numpy():
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        np_tpl = np.empty((L,L), dtype=f8)
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = np.empty_like(np_tpl)
            str(arr[0])
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
   
def test_calloc():
    cdef double *arr
    cdef list ts = []
    cdef list _ts
    cdef int i
    cdef long t1, t2
    for L in Ls:
        _ts = []
        for i in range(N):
            gc.disable()
            t1 = clock()
            for j in range(30):
                arr = <double*>malloc(sizeof(double*) * L * L)
                free(arr)
            t2 = clock()
            gc.enable()
            _ts.append((t2-t1)/30.)
        ts.append(float(np.median(_ts)))
    return ts
In [7]:
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')

import pandas as pd
pd.options.display.float_format = '{:.3f}'.format
from IPython.display import display

plt.figure(figsize=(12,6))
ts_arraybuffer = test_arraybuffer()
ts_cvarray = test_cvarray()
ts_numpy = test_numpy()
ts_calloc = test_calloc()

df = pd.DataFrame(
    {
        'L':Ls,
        'calloc': ts_calloc,
        'arraybuffer': ts_arraybuffer,
        'cvarray': ts_cvarray,
        'numpy': ts_numpy,
    }, 
    columns = ['L', 'calloc', 'arraybuffer', 'cvarray', 'numpy']
)
df = df.set_index('L')
display(df.transpose())

plt.plot(Ls, ts_calloc, marker='o', label='calloc', markersize=10)
plt.plot(Ls, ts_arraybuffer, marker='o', label='arraybuffer', markersize=10)
plt.plot(Ls, ts_cvarray, marker='o', label='cvarray', markersize=10)
plt.plot(Ls, ts_numpy, marker='o', label='numpy', markersize=10)
plt.semilogx()
plt.semilogy()
plt.grid()
plt.xlabel('L')
plt.ylabel('time')
plt.legend(loc='best')
plt.show()
L 1 10 20 30 40 50 60 70 80 90 100
calloc 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.067 0.100
arraybuffer 0.200 0.267 0.200 0.200 0.200 0.200 0.200 0.200 0.200 0.200 0.200
cvarray 1.000 1.000 0.900 0.767 0.867 0.800 0.467 0.467 0.467 0.467 0.500
numpy 10.667 20.283 27.100 35.533 48.200 89.950 110.500 114.583 89.883 99.583 102.733
In [ ]: