<jupyter_start><jupyter_text>Optimisation de code avec cffi, numba, cythonL'idée est de recoder une fonction en C. On prend comme exemple la fonction de prédiction de la régression linéaire de [scikit-learn](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html) et de prévoir le gain de temps qu'on obtient en recodant la fonction dans un langage plus rapide.<jupyter_code>from jyquickhelper import add_notebook_menu
add_notebook_menu()
memo_time = []
import timeit

def unit(x):
    if x >= 1: return "%1.2f s" % x
    elif x >= 1e-3: return "%1.2f ms" % (x* 1000)
    elif x >= 1e-6: return "%1.2f µs" % (x* 1000**2)
    elif x >= 1e-9: return "%1.2f ns" % (x* 1000**3)
    else:
        return "%1.2g s" % x

def timeexe(legend, code, number=100, repeat=10000):
    rep = timeit.repeat(code, number=number, repeat=repeat, globals=globals())
    ave = sum(rep) / (number * repeat)
    std = (sum((x/number - ave)**2 for x in rep) / repeat)**0.5
    fir = rep[0]/number
    fir3 = sum(rep[:3]) / (3 * number)
    las3 = sum(rep[-3:]) / (3 * number)
    rep.sort()
    mini = rep[len(rep)//20] / number
    maxi = rep[-len(rep)//20] / number
    print("Moyenne: %s Ecart-type %s (with %d runs) in [%s, %s]" % (
                unit(ave), unit(std), number, unit(mini), unit(maxi)))
    return dict(legend=legend, average=ave, deviation=std, first=fir, first3=fir3,
                last3=las3, repeat=repeat, min5=mini, max5=maxi, code=code, run=number)<jupyter_output><empty_output><jupyter_text>Régression linéaire<jupyter_code>from sklearn.datasets import load_diabetes
diabetes = load_diabetes()
diabetes_X_train = diabetes.data[:-20]
diabetes_X_test  = diabetes.data[-20:]
diabetes_y_train = diabetes.target[:-20]
diabetes_y_test  = diabetes.target[-20:]
from sklearn.linear_model import LinearRegression
clr = LinearRegression()
clr.fit(diabetes_X_train, diabetes_y_train)
clr.coef_
clr.intercept_
z = diabetes_X_test[0:1,:]
memo_time.append(timeexe("sklearn.predict", "clr.predict(z)"))
%timeit clr.predict(z)<jupyter_output>18.8 µs ± 907 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)<jupyter_text>optimisation avec cffiOn s'inspire de l'exemple [Purely for performance (API level, out-of-line)](http://cffi.readthedocs.io/en/latest/overview.html?highlight=examplepurely-for-performance-api-level-out-of-line).<jupyter_code>from cffi import FFI
ffibuilder = FFI()

ffibuilder.cdef("int linreg(int, double *, double *, double, double *);")

ffibuilder.set_source("_linear_regression",
r"""
    static int linreg(int dimension, double * x, double *coef, double intercept, double * out)
    {
        for(; dimension > 0; --dimension, ++x, ++coef)
            intercept += *x * *coef;
        *out = intercept;
        return 1;
    }
""")

ffibuilder.compile(verbose=True)<jupyter_output>generating .\_linear_regression.c
(already up-to-date)
running build_ext
building '_linear_regression' extension
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -Ic:\Python36_x64\include -Ic:\Python36_x64\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\ATLMFC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.6.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\winrt" /Tc_linear_regression.c /Fo.\Release\_linear_regression.obj
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\link.exe /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /LIBPATH:c:\Python36_x64\libs /LIBPATH:c:\Python36_x64\PCbuild[...]<jupyter_text>La fonction compilée est accessible comme suit.<jupyter_code>from _linear_regression import ffi, lib
lib.linreg<jupyter_output><empty_output><jupyter_text>On s'inspire de l'exemple [How to pass a Numpy array into a cffi function and how to get one back out?](https://stackoverflow.com/questions/16276268/how-to-pass-a-numpy-array-into-a-cffi-function-and-how-to-get-one-back-out).<jupyter_code>import numpy
out = numpy.zeros(1)
ptr_coef = clr.coef_.__array_interface__['data'][0]
cptr_coef = ffi.cast ( "double*" , ptr_coef )
x = diabetes_X_test[0:1,:]
ptr_x = x.__array_interface__['data'][0]
cptr_x = ffi.cast ( "double*" , ptr_x )
ptr_out = out.__array_interface__['data'][0]
cptr_out = ffi.cast ( "double*" , ptr_out )
n = len(clr.coef_)
lib.linreg(n, cptr_x, cptr_coef, clr.intercept_, cptr_out)
out<jupyter_output><empty_output><jupyter_text>On vérifie qu'on obtient bien la même chose.<jupyter_code>clr.predict(x)<jupyter_output><empty_output><jupyter_text>Et on mesure le temps d'exécution :<jupyter_code>memo_time.append(timeexe("cffi-linreg", "lib.linreg(n, cptr_x, cptr_coef, clr.intercept_, cptr_out)"))<jupyter_output>Moyenne: 441.64 ns Ecart-type 236.02 ns (with 100 runs) in [402.96 ns, 624.20 ns]<jupyter_text>C'est beaucoup plus rapide. Pour être totalement honnête, il faut mesurer les étapes qui consiste à extraire les pointeurs.<jupyter_code>def predict_clr(x, clr):
    out = numpy.zeros(1)
    ptr_coef = clr.coef_.__array_interface__['data'][0]
    cptr_coef = ffi.cast ( "double*" , ptr_coef )
    ptr_x = x.__array_interface__['data'][0]
    cptr_x = ffi.cast ( "double*" , ptr_x )  
    ptr_out = out.__array_interface__['data'][0]
    cptr_out = ffi.cast ( "double*" , ptr_out ) 
    lib.linreg(len(x), cptr_x, cptr_coef, clr.intercept_, cptr_out)
    return out

predict_clr(x, clr)
memo_time.append(timeexe("cffi-linreg-wrapped", "predict_clr(x, clr)"))<jupyter_output>Moyenne: 7.43 µs Ecart-type 2.28 µs (with 100 runs) in [6.36 µs, 12.22 µs]<jupyter_text>Cela reste plus rapide.  cffi - seconde versionComme on construit la fonction en dynamique (le code est connu lors de l'exécution), on peut facilement se passer de la boucle et écrire le code sans boucle et avec les coefficients.<jupyter_code>res = " + ".join("{0}*x[{1}]".format(c, i) for i, c in enumerate(clr.coef_))
res
code = """
    static int linreg_custom(double * x, double * out)
    {{
        out[0] = {0} + {1};
    }}
""".format(clr.intercept_, res)
print(code)
from cffi import FFI
ffibuilder = FFI()

ffibuilder.cdef("int linreg_custom(double *, double *);")
ffibuilder.set_source("_linear_regression_custom", code)
ffibuilder.compile(verbose=True)
from _linear_regression_custom.lib import linreg_custom
linreg_custom(cptr_x, cptr_out)
out
memo_time.append(timeexe("cffi-linreg-custom", "linreg_custom(cptr_x, cptr_out)"))<jupyter_output>Moyenne: 368.41 ns Ecart-type 204.57 ns (with 100 runs) in [288.40 ns, 513.58 ns]<jupyter_text>On a gagné un facteur 2.<jupyter_code>def predict_clr_custom(x):
    out = numpy.zeros(1)
    ptr_x = x.__array_interface__['data'][0]
    cptr_x = ffi.cast ( "double*" , ptr_x )  
    ptr_out = out.__array_interface__['data'][0]
    cptr_out = ffi.cast ( "double*" , ptr_out ) 
    linreg_custom(cptr_x, cptr_out)
    return out

predict_clr_custom(x)
memo_time.append(timeexe("cffi-linreg-custom wrapped", "predict_clr_custom(x)"))<jupyter_output>Moyenne: 5.39 µs Ecart-type 2.06 µs (with 100 runs) in [4.54 µs, 9.85 µs]<jupyter_text>C'est un peu plus rapide.  et en float?L'ordinateur fait la distinction entre les [double](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) code sur 64 bit et les [float](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) codé sur 32 bits. La précision est meilleure dans le premier cas et les calculs sont plus rapides dans le second. Dans le cas du machine learning, on préfère la rapidité à une perte précision en précision qui est souvent compensée par l'optimisation inhérente à tout problème de machine learning. Ce qu'on perd sur une observation, on le retrouve sur une autre.<jupyter_code>res = " + ".join("{0}f*x[{1}]".format(c, i) for i, c in enumerate(clr.coef_))
res
code = """
    static int linreg_custom_float(float * x, float * out)
    {{
        out[0] = {0}f + {1};
    }}
""".format(clr.intercept_, res)
print(code)
from cffi import FFI
ffibuilder = FFI()

ffibuilder.cdef("int linreg_custom_float(float *, float *);")
ffibuilder.set_source("_linear_regression_custom_float", code)
ffibuilder.compile(verbose=True)
from _linear_regression_custom_float.lib import linreg_custom_float
def predict_clr_custom_float(x):
    out = numpy.zeros(1, dtype=numpy.float32)
    ptr_x = x.__array_interface__['data'][0]
    cptr_x = ffi.cast ( "float*" , ptr_x )  
    ptr_out = out.__array_interface__['data'][0]
    cptr_out = ffi.cast ( "float*" , ptr_out ) 
    linreg_custom_float(cptr_x, cptr_out)
    return out<jupyter_output><empty_output><jupyter_text>Avant d'appeler la fonction, on doit transformer le vecteur iniatial en [float32](https://docs.scipy.org/doc/numpy/user/basics.types.html).<jupyter_code>x32 = x.astype(numpy.float32)
predict_clr_custom(x32)
memo_time.append(timeexe("cffi-linreg-custom-float wrapped", "predict_clr_custom(x32)"))<jupyter_output>Moyenne: 5.32 µs Ecart-type 1.36 µs (with 100 runs) in [4.66 µs, 8.33 µs]<jupyter_text>La différence n'est pas flagrante. Mesurons le code C uniquement même si la partie Python ne peut pas être complètement évitée.<jupyter_code>out = numpy.zeros(1, dtype=numpy.float32)
ptr_x = x32.__array_interface__['data'][0]
cptr_x = ffi.cast ( "float*" , ptr_x )  
ptr_out = out.__array_interface__['data'][0]
cptr_out = ffi.cast ( "float*" , ptr_out ) 

memo_time.append(timeexe("cffi-linreg-custom-float32", "linreg_custom_float(cptr_x, cptr_out)"))<jupyter_output>Moyenne: 315.92 ns Ecart-type 118.22 ns (with 100 runs) in [284.44 ns, 493.83 ns]<jupyter_text>La différence n'est pas significative.  SIMDC'est un ensemble d'instructions processeur pour faire des opérations terme à terme sur 4 float32 aussi rapidement qu'une seule. Le processeur ne peut faire des opérations que les nombres sont copiés dans ses registres. Le programme passe alors son temps à copier des nombres depuis la mémoire vers les registres du processeur puis à faire la copie dans le chemin inverse pour le résultat. Les instructions [SIMD](https://en.wikipedia.org/wiki/SIMD) font gagner du temps du niveau du calcul. Au lieu de faire 4 opérations de multiplication terme à terme, il n'en fait plus qu'une. Il suffit de savoir comment utiliser ces instructions. Avec Visual Studio, elles sont accessible via ces fonctions [Memory and Initialization Using Streaming SIMD Extensions](https://msdn.microsoft.com/en-us/library/0hey67c0%28v=vs.100%29.aspx).Le code suivant n'est probablement pas optimal mais il n'est pas trop compliqué à suivre.<jupyter_code>code = """
#include <xmmintrin.h> 

static int linreg_custom_float_simd(float * x, float * out)
{
    __m128 c1 = _mm_set_ps(0.3034995490664121f, -237.63931533353392f, 510.5306054362245f, 327.7369804093466f);
    __m128 c2 = _mm_set_ps(-814.1317093725389f, 492.81458798373245f, 102.84845219168025f, 184.60648905984064f);
    __m128 r1 = _mm_set_ss(152.76430691633442f);
    r1 = _mm_add_ss(r1, _mm_mul_ps(c1, _mm_load_ps(x)));
    r1 = _mm_add_ss(r1, _mm_mul_ps(c2, _mm_load_ps(x+4)));
    float r[4];
    _mm_store_ps(r, r1);    
    out[0] = r[0] + r[1] + r[2] + r[3] + 743.5196167505419f * x[8] + 76.095172216624f * x[9];
    return 1;
}
"""
from cffi import FFI
ffibuilder = FFI()

ffibuilder.cdef("int linreg_custom_float_simd(float *, float *);")
ffibuilder.set_source("_linear_regression_custom_float_simd", code)
ffibuilder.compile(verbose=True)
from _linear_regression_custom_float_simd.lib import linreg_custom_float_simd
out = numpy.zeros(1, dtype=numpy.float32)
ptr_x = x32.__array_interface__['data'][0]
cptr_x = ffi.cast ( "float*" , ptr_x )  
ptr_out = out.__array_interface__['data'][0]
cptr_out = ffi.cast ( "float*" , ptr_out ) 

linreg_custom_float_simd(cptr_x, cptr_out)
out
memo_time.append(timeexe("cffi-linreg-custom-float32-simd", "linreg_custom_float_simd(cptr_x, cptr_out)"))<jupyter_output>Moyenne: 361.48 ns Ecart-type 324.13 ns (with 100 runs) in [280.49 ns, 513.58 ns]<jupyter_text>C'est légèrement mieux, quelques références :* [aligned_vs_unaligned_load.c](https://gist.github.com/rmcgibbo/7689820) : c'est du code mais facile à lire.* [How to Write Fast Numerical Code](https://www.inf.ethz.ch/personal/markusp/teaching/263-2300-ETH-spring11/slides/class17.pdf)Les processeurs évoluent au fil du temps, 4 float, 8 float, [SIMD2](https://msdn.microsoft.com/en-us/library/kcwz153a%28v=vs.100%29.aspx), [FMA4 Intrinsics Added for Visual Studio 2010 SP1](https://msdn.microsoft.com/en-us/library/gg445134%28v=vs.100%29.aspx), [AVX](https://software.intel.com/en-us/articles/introduction-to-intel-advanced-vector-extensions/).  Réécriture purement PythonOn continue avec uniquement du Python sans numpy.<jupyter_code>coef = clr.coef_
list(coef)
code = str(clr.intercept_) + "+" + "+".join("x[{0}]*({1})".format(i, c) for i, c in enumerate(coef))
code
def predict_clr_python(x):
    return 152.764306916+x[0]*0.3034995490664121+x[1]*(-237.63931533353392)+x[2]*510.5306054362245+ \
            x[3]*327.7369804093466+ \
            x[4]*(-814.1317093725389)+x[5]*492.81458798373245+x[6]*102.84845219168025+ \
            x[7]*184.60648905984064+x[8]*743.5196167505419+x[9]*76.095172216624
            
predict_clr_python(x[0])
z = list(x[0])
memo_time.append(timeexe("python-linreg", "predict_clr_python(z)"))<jupyter_output>Moyenne: 3.24 µs Ecart-type 492.98 ns (with 100 runs) in [2.93 µs, 4.35 µs]<jupyter_text>De façon assez surprenante, c'est plutôt rapide. Et si on y mettait une boucle.<jupyter_code>list(coef)
def predict_clr_python_loop(x):
    coef = [0.30349954906641208,
            -237.63931533353392,
            510.5306054362245,
            327.73698040934659,
            -814.13170937253892,
            492.81458798373245,
            102.84845219168025,
            184.60648905984064,
            743.5196167505419,
            76.095172216623993]
    return 152.764306916 + sum(a*b for a, b in zip(x, coef))
            
predict_clr_python_loop(x[0])
memo_time.append(timeexe("python-linreg-custom", "predict_clr_python_loop(z)"))<jupyter_output>Moyenne: 4.44 µs Ecart-type 1.12 µs (with 100 runs) in [3.92 µs, 6.90 µs]<jupyter_text>A peine plus long. Et si on sortait les coefficients de la fonction.<jupyter_code>coef = [0.30349954906641208,
        -237.63931533353392,
        510.5306054362245,
        327.73698040934659,
        -814.13170937253892,
        492.81458798373245,
        102.84845219168025,
        184.60648905984064,
        743.5196167505419,
        76.095172216623993]

def predict_clr_python_loop2(x):
    return 152.764306916 + sum(a*b for a, b in zip(x, coef))
            
predict_clr_python_loop2(x[0])
memo_time.append(timeexe("python-linreg-custom-static", "predict_clr_python_loop2(z)"))<jupyter_output>Moyenne: 4.32 µs Ecart-type 793.04 ns (with 100 runs) in [3.88 µs, 5.33 µs]<jupyter_text>Réécriture avec Python et numpy<jupyter_code>coef = numpy.array([0.30349954906641208,
        -237.63931533353392,
        510.5306054362245,
        327.73698040934659,
        -814.13170937253892,
        492.81458798373245,
        102.84845219168025,
        184.60648905984064,
        743.5196167505419,
        76.095172216623993])

def predict_clr_numpy(x):    
    return 152.764306916 + numpy.dot(coef, x).sum()
            
predict_clr_numpy(x[0])
memo_time.append(timeexe("numpy-linreg-custom-static", "predict_clr_numpy(z)"))<jupyter_output>Moyenne: 5.23 µs Ecart-type 2.13 µs (with 100 runs) in [4.44 µs, 8.59 µs]<jupyter_text>Les dimensions des tableaux sont trop petites pour que le calcul matriciel apporte une différence. On se retrouve dans le cas *cffi* où les échanges Python - C grignotent tout le temps de calcul.  numba[numba](http://numba.pydata.org/) essaye de compiler à la volée des bouts de codes écrits en Python. On induque quelle fonction optimiser en faisant précéder la fonction de ``@jit``. Toutes les écritures ne fonctionnent, typiquement, certaines listes en compréhension soulèvent une exception. Il faut donc écrire son code en Python d'une façon assez proche de ce qu'il serait en C.<jupyter_code>from numba import jit
@jit
def predict_clr_numba(x):
    coef = [0.30349954906641208,
            -237.63931533353392,
            510.5306054362245,
            327.73698040934659,
            -814.13170937253892,
            492.81458798373245,
            102.84845219168025,
            184.60648905984064,
            743.5196167505419,
            76.095172216623993]
    s = 152.764306916
    for i in range(0, len(x)):
        s += coef[i] * x[i]
    return s
            
predict_clr_numba(z)
memo_time.append(timeexe("numba-linreg", "predict_clr_numba(z)"))<jupyter_output>Moyenne: 1.03 µs Ecart-type 297.64 ns (with 100 runs) in [932.35 ns, 1.34 µs]<jupyter_text>Plutôt rapide !<jupyter_code>@jit('double(double[:])')
def predict_clr_numba_cast(x):
    coef = [0.30349954906641208,
            -237.63931533353392,
            510.5306054362245,
            327.73698040934659,
            -814.13170937253892,
            492.81458798373245,
            102.84845219168025,
            184.60648905984064,
            743.5196167505419,
            76.095172216623993]
    s = 152.764306916
    for i in range(0, len(x)):
        s += coef[i] * x[i]
    return s
            
# La fonction ne fonctionne qu'avec un numpy.array car le langage C est fortement typé.
predict_clr_numba_cast(x[0])
memo_time.append(timeexe("numba-linreg-type", "predict_clr_numba_cast(x[0])"))<jupyter_output>Moyenne: 783.52 ns Ecart-type 270.76 ns (with 100 runs) in [695.31 ns, 1.23 µs]<jupyter_text>On voit que plus on donne d'information au compilateur, plus il est capable d'optimiser.<jupyter_code>@jit('float32(float32[:])')
def predict_clr_numba_cast_float(x):
    coef = [0.30349954906641208,
            -237.63931533353392,
            510.5306054362245,
            327.73698040934659,
            -814.13170937253892,
            492.81458798373245,
            102.84845219168025,
            184.60648905984064,
            743.5196167505419,
            76.095172216623993]
    s = 152.764306916
    for i in range(0, len(x)):
        s += coef[i] * x[i]
    return s
            
# La fonction ne fonctionne qu'avec un numpy.array car le langage C est fortement typé.
xx = x[0].astype(numpy.float32)
predict_clr_numba_cast_float(xx)
memo_time.append(timeexe("numba-linreg-type-float32", "predict_clr_numba_cast_float(xx)"))<jupyter_output>Moyenne: 548.29 ns Ecart-type 361.07 ns (with 100 runs) in [497.78 ns, 857.28 ns]<jupyter_text>On se rapproche des temps obtenus avec *cffi* sans *wrapping*, cela signifie que *numba* fait un bien meilleur travail à ce niveau que le wrapper rapidement créé.<jupyter_code>coef = numpy.array([0.30349954906641208,
    -237.63931533353392,
    510.5306054362245,
    327.73698040934659,
    -814.13170937253892,
    492.81458798373245,
    102.84845219168025,
    184.60648905984064,
    743.5196167505419,
    76.095172216623993])

@jit('double(double[:])')
def predict_clr_numba_numpy(x):    
    return 152.764306916 + numpy.dot(coef, x).sum()

predict_clr_numba_numpy(x[0])
memo_time.append(timeexe("numba-linreg-type-numpy", "predict_clr_numba_numpy(x[0])"))<jupyter_output>Moyenne: 5.13 µs Ecart-type 1.71 µs (with 100 runs) in [4.51 µs, 7.03 µs]<jupyter_text>*numba* est moins performant quand *numpy* est impliqué car le code de numpy n'est pas réécrit, il est appelé.  cython[cython](http://cython.org/) permet de créer des extensions C de plus grande envergure que *numba*. C'est l'option choisie par [scikit-learn](http://scikit-learn.org/stable/). Il vaut mieux connaître le C pour s'en servir et là encore, l'objectif est de réduire les échanges Python / C qui coûtent cher.<jupyter_code>%load_ext cython
%%cython
def predict_clr_cython(x):
    coef = [0.30349954906641208,
            -237.63931533353392,
            510.5306054362245,
            327.73698040934659,
            -814.13170937253892,
            492.81458798373245,
            102.84845219168025,
            184.60648905984064,
            743.5196167505419,
            76.095172216623993]
    s = 152.764306916
    for i in range(0, len(x)):
        s += coef[i] * x[i]
    return s
predict_clr_cython(x[0])
memo_time.append(timeexe("cython-linreg", "predict_clr_cython(x[0])"))<jupyter_output>Moyenne: 3.10 µs Ecart-type 413.13 ns (with 100 runs) in [2.89 µs, 3.85 µs]<jupyter_text>Cython fait moins bien que *numba* dans notre cas et l'optimisation proposée est assez proche du temps déjà obtenue avec le langage Python seul. Cela est dû au fait que la plupart des objets tels que du code associé aux listes ou aux dictionnaires ont été réécrits en C.<jupyter_code>%%cython
def predict_clr_cython_type(x):
    cdef double[10] coef;
    coef[0] = 0.30349954906641208;
    coef[1] = -237.63931533353392;
    coef[2] = 510.5306054362245;
    coef[3] = 327.73698040934659;
    coef[4] = -814.13170937253892;
    coef[5] = 492.81458798373245;
    coef[6] = 102.84845219168025;
    coef[7] = 184.60648905984064;
    coef[8] = 743.5196167505419;
    coef[9] = 76.095172216623993;
    cdef double s = 152.764306916
    for i in range(0, len(x)):
        s += coef[i] * x[i]
    return s
predict_clr_cython_type(x[0])
memo_time.append(timeexe("cython-linreg-type", "predict_clr_cython_type(x[0])"))<jupyter_output>Moyenne: 3.35 µs Ecart-type 541.78 ns (with 100 runs) in [3.08 µs, 4.60 µs]<jupyter_text>Récapitulatif<jupyter_code>import pandas
df = pandas.DataFrame(data=memo_time)
df = df.set_index("legend").sort_values("average")
df<jupyter_output><empty_output><jupyter_text>On enlève quelques colonnes et on rappelle :* **cffi**: signifie optimisé avec cffi* **custom**: pas de boucle mais la fonction ne peut prédire qu'une seule régression linéaire* **float32**: utilise des float et non des double* **linreg**: régression linéaire* **numba**: optimisation avec numba* **numpy**: optimisation avec numpy* **python**: pas de C, que du python* **simd**: optimisé avec les instructions SIMD* **sklearn**: fonction sklearn.predict* **static**: la fonction utilise des variables statiques* **type**: la fonction est typée et ne fonctionne qu'avec un type précis en entrée.* **wrapped**: code optimisé mais embabllé dans une fonction Python qui elle ne l'est pas (les containers sont recréés à chaque fois)<jupyter_code>cols = ["average", "deviation", "min5", "max5", "run", "code"]
df[cols]
%matplotlib inline
df[["average"]].plot(kind="barh", figsize=(10,4), logx=True)<jupyter_output><empty_output><jupyter_text>Il manque à ce comparatif le [GPU](https://en.wikipedia.org/wiki/Graphics_processing_unit) mais c'est un peu plus complexe à mettre en oeuvre et surtout il faut une carte [GPU](https://fr.wikipedia.org/wiki/Processeur_graphique).  Prédiction one-off et biais de mesureLe graphique précédent montre que la fonction ``predict`` de *scikit-learn* est la plus lente. La première raison est que ce code est valable pour toutes les régresssions linéaires alors que toutes les autres fonctions sont spécialisées pour un seul modèle. La seconde raison est que le code de *scikit-learn* est optimisé pour le calcul de plusieurs prédictions à la fois alors que toutes les autres fonctions n'en calcule qu'une seule (scénario dit *one-off*).<jupyter_code>memo = []
dims = [1, 10, 100, 200, 500, 1000, 2000, 3000, 4000, 5000, 10000, 20000, 50000]
for i in dims:
    if i <= diabetes_X_test.shape[0]:
        mx = diabetes_X_test[:i]
    else:
        mxs = [diabetes_X_test] * (i // diabetes_X_test.shape[0] + 1)
        mx = numpy.vstack(mxs)
        mx = mx[:i]
    memo.append(timeexe("sklearn.predict %d" % i, "clr.predict(mx)", repeat=100))
df = pandas.DataFrame(memo).set_index("legend")
df["dims"] = dims
df["ave_per_obs"] = df["average"] / df["dims"]
df[["average", "dims", "ave_per_obs"]]
df[["dims", "ave_per_obs"]].plot(x="dims", y="ave_per_obs", logy=True, logx=True)<jupyter_output><empty_output><jupyter_text>Le minimum obtenu est pour $10^{-8} s$ soit 10 ns. Cela montre que la comparaisson précédente était incomplète voire biaisée. Tout dépend de l'usage qu'on fait de la fonction de prédiction même s'il sera toujours possible de d'écrire un code spécialisé plus rapide que toute autre fonction générique. En général, plus on reste du côté Python, plus le programme est lent. Le nombre de passage de l'un à l'autre, selon la façon dont il est fait ralenti aussi. En tenant compte de cela, le programme rouge sera plus lent que le vert.<jupyter_code>from pyquickhelper.helpgen import NbImage
NbImage("pycpp.png")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12465195109156209, 'fraction_numerical': 0.1105685807675235, 'mean_word_length': 4.288518992744345, 'pattern_counts': {'":': 0, '<': 96, '<?xml version=': 0, '>': 100, 'https://': 13, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '352444', 'license': 'MIT', 'n_tokens_mistral': 11245, 'n_tokens_neox': 9641, 'n_words': 2179, 'path': '_doc/notebooks/2a/cffi_linear_regression.ipynb', 'repo_name': 'mohamedelkansouli/Ensae_py', 'size': '24781'}