C++/STL vs Python/NumPy

Ce TP a pour objectif de d'explorer les possibilités de deux langages différents, C++ et Python, de gérer, stocker, manipuler des ensembles/tableaux de grande taille :

  • en C++ avec la stl et plus particulièrement les std::vector;
  • en Python avec les array de numpy.


La STL en C++ (standard template library) implémente, entre autre, des containers génériques (vecteurs, listes chaînées, ensembles ordonnés) génériques, simple d'utilisation et efficaces. La STL fournit en plus des algorithmes permettent de manipuler aisément ces containers (initialiser, rechercher, trier, …). La STL introduit également le concept d'iterator qui permet de parcourir très facilement un container en s'affranchissant complètement de la manière dont il est implémenté.


Numpy est une lib python destinée à la manipulation de grands ensembles de nombres et est très utilisée par la communauté scientifique.Elle propose des types et des opérations beaucoup plus performants que ceux de la lib standard, et possède des raccourcis pour les traitements de masse. C’est une lib puissante mais également complexe par la multitude de fonctions disponibles.



Soit vous installez python sur votre ordinateur.

  • sudo apt install python3 ou https://www.python.org/downloads/
  • Pour ceux qui pense faire plus de python : anaconda vous permet d'avoir plusieurs environnements (versions, lib, etc.) de manière étanche

Pour tout faire en ligne.



Prise en main de Numpy

En C++ vous avez les tableaux du langages et les std::vector de la STL.

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

int main()
{
	int* tl = new int[3];            // tableau dynamique alloué sur le tas (il faut le libérer)
	tl[0] = 1;
	delete[] tl;

	std::vector<int> ts = { 1,2,3 };          // vector STL alloué de manière cachée dans le tas, le vector gère son allocation et destruction
	std::copy(ts.begin(), ts.end(), std::ostream_iterator<int>(std::cout, " "));
}


En Python, de manière similaire, vous avez les list standard et les ndarray fouri par Numpy. ndarray est un type de tableau optimisé qui se manipule comme une tuple, avec une différence majeure : il ne peut contenir qu’un seul type de données. Donc on ne met que des int, ou que des str, que des bool, etc.

>>> import numpy as np
>>> a = [1,3,6]
>>> type(a)
<class 'list'>

>>> b = np.array([1,2,3])
>>> type(b)
<class 'numpy.ndarray'>
>>> print(b)
[1 2 3]


Tableau 1D

Remplir

Pour remplir un ndarray souvent on utilisera une fonction générant son contenu automatiquement afin d’éviter de créer deux structures de données (la liste, puis l’array par exemple).


A faire. Remplissez un tableau a avec des nombres allant de 100 à 200 par pas de 4 et affichez son contenu.

En C++ :

   ? proposez une solution élégante, sans boucle for ?
   std::copy(a.begin(),a.end(),std::ostream_iterator<int>(std::cout," "));    // l'affichage

Avec Numpy, regardez la documentation de arrange.

?
print(a)  


A faire. Remplissez un 2e tableau b de 20 réels compris entre 27 et 44 par interpolation linéaire.

En Python, regardez linspace.

?  (facile)


Plus généralement, avec Numpy il existe de nombreuses fonctions pour remplir un tableau, regardez ici. En C++ souvent on écrit la bonne fonction bien comme il faut. Ici on va écrire un objet fonction Générateur et l'utiliser avec un genera_n et un inserter ou directement un generate si votre tableau a déjà la bonne taille.

class Gen
{
public:
   Gen...
   int operator()() { ... return res; }
};

...
   vector<float a;
   generate_n(  inserter(a, a.begin()), 20, Gen( ... ));     // on agrandit le tableau avec l'inserter
   
   vector<float> b(20);                           // tableau de 20 cases
   generate(  b.begin(), b.end(), Gen( ... ));    // on remplit les 20 cases


Compter

A faire. Comptez le nombre de chiffres pairs dasn le tableau.


En C++, regardez count_if.

En Python, proposez plusieurs manières de faire et chronométrez le temps pour chacune avec un tableau de grande taille. Pour vous inspirez voici comment compter le nombre de nombres compris entre 25 et 100 avec les temps de calcul. Vous pourrez constater qu'elle ne sont pas toutes équivariantes en terme de performance (sur 10 millions d’éléments dans le tableau) :

ni = ((25 < a) & (a < 50)).sum()
 chrono=0.07596540451049805

ni = np.count_nonzero((25 < a) & (a < 50))
 chrono=0.05292034149169922

ni = a[(25 < a) & (a < 50)].size
 chrono=0.10593771934509277

ni = len([x for x in a.ravel() if 25 < x < 50])
 chrono=5.013119220733643

ni = sum(1 for i in a.ravel() if 25 < i < 50)
chrono=4.8692097663879395

Pour chronométrer :

t0 = time.time()
...
duree = time.time()-t0
print("chrono="+str(duree))


Tableau 2D

En Python pour créer un tableau à 2 dimensions

a = np.empty( (4,6), dtype=int)       # un tableau vide d'entier (il y a n'importe quoi dans les cases
a = np.zeros( (4,6), dtype=int)       # un tableau rempli de 0

La fonction size renvoie la taille total du tableau (dim1 x dim2) alors que shape donne les 2 dimensions dans un tableau.

>>> a = np.zeros( (4,6), dtype=int )
>>> a.size
24
>>> a.shape
(4, 6)
>>> a.shape[0]
4

En C++ les tableaux multi dimensions peuvent se créer comme ceci.

    vector< vector<int> > vec(4, vector<int>(6));
    vec[2][3] = 10;
    int a = vec[2][3];

Attention, chaque sous vector peut avoir une taille différente si vous le redimensionnez. Souvent en C++ le développeur réécrit une classe vector2d ou vectorNd. Il y a de nombreuses autres manières de faire, comme par exemple le tableau 1D avec accès par y*DIMX+x, etc.


A faire. Créer un tableau 2D de taille (50,25) qui comportera dans chaque case (i,j) la valeur i^2 + 5j - 4. Afficher sa taille totale, la taille des 2 dimensions, le nombre de cases impairs du tableau et afficher tout le tableau.


Avantage Numpy qui offre une gestion des tableaux multi-dimensionnels facilement par rapport à C++ ou même à Java ou C#.


Apply/ Vectorize / Lambda function

Dans les deux langages, vous pouvez appliquer une opération sur vos données en une seule ligne. En C++ avec for_each ou avec transform.

void myfunction (int i) {  // function:
  std::cout << ' ' << i;
}

int op_increase (int i) { return i+1; }

int main()
{
   std::vector<int> myvector, res;
   ...
   res.resize(foo.size());   
   std::transform (myvector.begin(), myvector.end(), res.begin(), op_increase);
   ...
   std::cout << "res contains:";
   for_each (res.begin(), res.end(), myfunction);
   std::cout << '\n';
   ...

En Python, la fonction vectorize evalue une fonction sur tous les éléments d'un tableau Numpy en entrée comme la fonction map de python sur les listes.

def myfunc(a, b):
    if a > b:
        return a - b
     else:
         return a + b
vfunc = np.vectorize(myfunc)
vfunc([1, 2, 3, 4], 2)     #donne array([3, 4, 1, 2])

Mais le plus efficace est de faire ceci avec une lambda function

carre = lambda x: x**2   # renvoie x^2
a = carre(a.all())


En Python, la fonction apply_along_axis travaille sur une des dimensions, un des axes.


A faire. Dans les 2 langages incrémentés tous les nombres impairs du tableau 2D crée à la question précédente afin que tous les nombres deviennent pairs.



Slicing

Lors de la manipulation des tableaux, on a souvent besoin de récupérer une partie d’un tableau.

Pour cela, Python permet d’extraire des tranches d’un tableau grâce une technique appelée slicing (tranchage, en français). Elle consiste à indiquer entre crochets des indices pour définir le début et la fin de la tranche et à les séparer par deux-points :.

>>> a = np.array([127, 25, 34, 561, 87,6, 123])
>>> a[1:3]
array([25, 34])

La borne début ou de fin peut être vide.

>>> b=a[4:]
>>> b
array([ 87,   6, 123])

Vous obtenez une copie de la sous partie du tableau. Si vous changez une des case, le tableau original n'est pas touché.

>>> b[0] = 111
>>> a
array([127,  25,  34, 561, 111,   6, 123])

Vous pouvez avoir un pas différent pour ne prendre qu'une case sur deux :

>>> b = a[::2]
>>> b
array([127,  34, 111, 123])


Pour cela, C++ propose soit de passer par des itérateurs particuliers (quand on ne veut que consulter), soit de se baser sur les type valarray et la fonction gslice. Mais il faut avouer que les valarray ne sont que très peu utilisés. En général, le développeur se réécrit sa propre fonction de slicing.

template<typename T>
std::vector<T> slicing(const std::vector& v, int first, int last, int step)



Le slicing est un outil vraiment sympa et puissant, couramment utilisé en traitement des données. Attention cependant à ce que votre code reste lisible et compréhensible, surtout avec des tableaux 2D, 3D, etc.


Mesures de performance

En C++ et en Python remplissez un tableau 2D patde NP patients comportant NT relevés de températures. La température est calculée par une interpolation linéaire entre 36 et 39. Puis calculez la température moyenne des NP patients et la ranger dans un nouveau tableau tempe. En Python vous avez la funtion sum et en C++ la fonction accumulate.


En Python pour chronométrer

t0 = time.time()
... le truc à chronométrer ...
duree = time.time()-t0
print("chrono="+str(duree))

En C++

  auto start = std::chrono::system_clock::now();
  
  std::cout << "f(42) = " << fibonacci(42) << '\n';
  
  auto end = std::chrono::system_clock::now();
  
  std::cout << "elapsed time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count() << "ms\n";  
 // OU
  std::cout << "elapsed time: " << std::chrono::duration_cast<std::chrono::seconds>(end-start).count() << "s\n";  



Sur ma machine, avec NP=5000 et NT=100000 (remplissage de 5000×100000 nombres puis 5000 calculs de moyenne sur 100000 nombres),
Python

***creation = 1.3624656200408936 secondes
***calcul   = 1.568084478378296 secondes

C++ en -O3 (temps similaire entre g++ et visual studio)

***Creation : 377ms
***Calcul   : 213ms

Avantage C++ qui est 4 à 5 fois plus rapide que Python.



Conclusion

La conclusion de ce TP pourrait être la suivante.

  • Avantage Numpy qui offre une gestion des tableaux multi-dimensionnels facilement par rapport à C++.
  • Avantage C++ qui est 4 à 5 fois plus rapide que Python./Numpy

Pour écrire des prototype rapide de gestion de données, Python permet d'écrire du code plus vite. C++ reste plus rapide si vous voulez des perf (éventuellement avec Cuda en plus).

Une bonne solution semble être l'écriture du noyau de votre application en C++ puis une surcouche en Python pour facilité le développement de proto (voir Swig pour interfacer les 2 langages). C'est ce choix qui est fait par exemple dans de nombreuses plateformes de Deep Learning.



Pour aller plus loin


Calcul matriciel

Avec vos tableaux 2D en C++ et en Python, définissez deux matrices de taille NxN et écrivez l'opérateur de multiplication. Mesurez les performances de la multiplications entre ces 2 tableaux.