Commit 0bcc725d authored by CHAMONT David's avatar CHAMONT David
Browse files

un peu de nettoyage et utilisation du registry gitlab

parent 1ca16e99
# bSIMD
EN CONSTRUCTION
# Vectorisation avec Boost.SIMD
Single instruction, multiple data (SIMD) instructions or multimedia extensions have been available for many years. They are designed to significantly accelerate code execution, however they require expertise to be used correctly, depends on non-uniform compiler support, the use of low-level intrinsics, or vendor-specific libraries.
bSIMD is a C++ library which aims to simplify the error-prone process of developing applications using SIMD instructions sets. bSIMD is designed to seamlessly integrate into existing projects so that you can quickly and easily start developing high performance, portable and future proof software.
Cette bibliothèque a pour objectif de faciliter l'exploitation des registres vectoriels des processeurs modernes (SIMD). Actuellement distribuée au sein de NT2, elle sera proposée comme nouvelle bibliothèque à la communauté BOOST.
* prérequis : connaissance du C++ moderne.
* hauteur du plongeoir : ?.
* préinstallation : Docker, image `piscineri3/gcc61:3`.
* préinstallation : Docker, image `gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd`.
* fichiers : https://gitlab.in2p3.fr/MaitresNageurs/PiscineJI/tree/master/CalculBsimd
* maître(s) nageur(s) : [David Chamont](http://informatique.in2p3.fr/?q=user/3).
* <img src="img/david.jpeg" height=50>
......@@ -22,8 +19,9 @@ habituels, et d'utiliser une machine Docker uniquement pour compiler et exécute
le programme résultant.
Vous devriez avoir déjà installé Docker, conformément aux instructions du [README.md](../README.md)
principal de ce projet. Vous devriez également déjà disposer de l'image `piscineri3/bsimd`. Si ce n'est pas le cas et
que vous disposez de réseau, tapez `docker pull piscineri3/bsimd`.
principal de ce projet. Vous devriez également déjà disposer de l'image `gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd`.
Si ce n'est pas le cas et que vous disposez de réseau, tapez
`docker pull gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd`.
En partant de l'hypothèse que vous éditerez vos fichiers dans le répertoire
local <LOCAL>, nous vous proposons de monter ce répertoire en tant que
......@@ -46,7 +44,7 @@ A présent, compilez le et exécutez le au sein d'une machine Docker
`piscineri3/bsimd` :
```shell
> docker run --rm -it -v <LOCAL>:/piscine piscineri3/bsimd /bin/bash
> docker run --rm -it -v <LOCAL>:/piscine gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd /bin/bash
>> cd /piscine
>> g++ -std=c++14 hello_world.cpp -o hello_world.exe
>> ./hello_world.exe
......@@ -58,323 +56,146 @@ Vous etes prêt.
---
## Inférence de type : le mot-clef `auto`
C++ est statiquement et fortement typé : le programmeur doit préciser au
compilateur le type de toutes les variables qu'il utilise, ce qui peut conduire
à un code difficile à écrire comme à lire.
## La notion de bloc (pack)
```c++
// Fichier auto_introduction_1.cpp
Le principal élément de Boost.SIMD est la classe `boost::simd::pack`, accessible via le fichier d'entête `boost/simd/sdk/simd/pack.hpp`.
#include <iostream>
#include <vector>
Une instance de `pack<T,N>` représente un bloc de `N` éléments de type `T`, de façon similaire à une instance de `std::array`. La principale différence, c'est que `boost::simd::pack` s'efforcera d'utiliser les registres et les instructions SIMD.
void affiche( std::vector<int> & col )
{
std::vector<int>::iterator itr = col.begin() ;
while (itr!=col.end())
{
std::cout<<i<<std::endl ;
++itr ;
}
}
int main()
{
std::vector<int> v {1,2,3,4,5} ;
affiche(v) ;
}
```
La classe `boost::simd::pack` propose de multiples constructeurs. Elle est évidemment copiable et constructible par défaut. Elle propose également de multiples méthodes d'initialisation du contenu.
Traditionnellement, on allège les notations en ayant recours aux alias
de types :
```c++
// Fichier auto_introduction_2.cpp
Dans l'exemple ci-dessous, nous utilisons les registres vectoriels pour calculer un jeu valeurs 42. Un typedef permet d'alléger l'écriture :
``` cpp
#include <boost/simd/sdk/simd/pack.hpp>
#include <boost/simd/sdk/simd/io.hpp>
#include <boost/simd/include/functions/splat.hpp>
#include <boost/simd/include/functions/plus.hpp>
#include <boost/simd/include/functions/multiplies.hpp>
#include <iostream>
#include <vector>
typedef std::vector<int> Collection ;
void affiche( Collection & col )
{
Collection::iterator itr = col.begin() ;
while (itr!=col.end())
{
std::cout<<i<<std::endl ;
++itr ;
}
}
int main()
{
Collection v {1,2,3,4,5} ;
affiche(v) ;
}
```
Cependant, on peut juger inutile de devoir préciser le type de la variable
`itr`, qui est le même que celui de l'expression servant à calculer
sa valeur initiale (`col.begin()`).
Dans ce cas de figure, C++11 permet d'utiliser le mot-clef `auto` à la place du type
de la variable, indiquant ainsi au compilateur de réutiliser le type de la valeur
initiale.
typedef boost::simd::pack<double> pdouble ;
int size = pdouble::static_size ;
```c++
// Fichier auto_introduction_3.cpp
std::cout << "pack size: " << size << std::endl ;
#include <iostream>
#include <vector>
pdouble res ;
pdouble u(10) ;
pdouble r = boost::simd::splat<pdouble>(11) ;
typedef std::vector<int> Collection ;
res = (u + r) * 2.f ;
void affiche( Collection & col )
{
auto itr = col.begin() ;
while (itr!=col.end())
{
std::cout<<i<<std::endl ;
++itr ;
}
}
std::cout << res << std::endl ;
int main()
{
Collection v {1,2,3,4,5} ;
affiche(v) ;
return 0 ;
}
```
Attention : lorsque vous utilisez `auto`, évitez d'utiliser en même temps l'initialisation
par accolades introduite par C++11 . Il existe en effet une possibilité d'interaction indésirable
entre ces deux nouveautés (`auto` risque de retenir `std::initializer_list` comme
type). Il est prévu de résoudre ce problème dans la norme C++17.
Le constructeur de `u` réplique la valeur scalaire `10` dans tous les éléments du bloc, de façon équivalente à l'instruction suivante, qui réplique la valeur `11` via la fonction `splat`. On peut également intialiser les élémets d'un bloc en énumérant ses valeurs :
On notera que `auto`, lorsqu'il est utilisé avec des patrons de fonction, permet
d'implémenter des choses impossibles auparavant :
```c++
// Fichier auto_introduction_4.cpp
#include <iostream>
template< typename LHS, typename RHS >
void affiche_division( LHS lhs, RHS rhs )
{
auto res = lhs/rhs ;
std::cout<<res<<std::endl ;
}
int main()
{
affiche_division(3.,2) ;
}
``` cpp
boost::simd::pack<float> r(11,11,11,11) ;
```
Tant que l'on ne connait pas `T1` et `T2`, impossible de connaitre le type de `lhs/rhs`.
Seul `auto` permet de déclarer `res` avec le type qui préservera toute la précision
utile du calcul.
---
## Déclaration à droite du type de retour
Une nouvelle façon de déclarer le type de retour s'appuie également sur
le mot-clef `auto`, placé avant la signature de la fonction, et l'opérateur `->`
placé après la signature :
```c++
// Fichier auto_return_1.cpp
Ce dernier constructeur fait l'hypothèse que le nombre de valeurs fournies correspond très exactement à la taille du bloc. Dans la mesure du possible, il faut éviter de dépendre d'une taille donnée pour `boost::simd::pack`.
#include <iostream>
Une fois initialisés, les blocs peuvent être manipulés de façon similaire à des valeurs scalaires, et tous les opérateurs et fonctions mathématiques ordinaires sont disponibles, à condition d'inclure le fichier d'entête approprié. Dans notre exemple, nous incluons `plus.hpp` et `multiplies.hpp` afin de pouvoir utiliser les opérateurs + et \*.
auto division( double lhs, double rhs ) -> double
{ return lhs/rhs ; }
int main()
{
std::cout<<division(3.,2)<<std::endl ;
}
``` cpp
res = (u + r) * 2.f ;
```
Certains programmeurs désapprouve le choix du comité de normalisation de
réutiliser le mot-clef `auto` pour ce mécanisme. En effet, il n'y
a ici aucune inférence de type.
Notez que la vérification de types est plus stricte que d'habitude quand des blocs SIMD entrent en jeu. Ci-dessus, on ne peut multiplier notre somme de blocs que par `2.f` et non par `2`.
Enfin, on affiche le contenu d'un bloc à l'aide de l'opérateur `<<`, mis à disposition par le fichier d'entête `boost/simd/sdk/simd/io.hpp`.
---
## Le mot-clé `decltype`
Plutôt que de laisser tout le travail au compilateur, on peut également
récupérer le type d'une expression quelconque grâce au mot-clef `decltype`,
et s'en servir pour déclarer une nouvelle variable :
```c++
int n ;
double x ;
decltype(2*x+n) z ; // z sera du type de 2*x+n, soit ici double
.....
std::map<int,std::string> m1 ;
decltype(m1) m2 ; // m2 aura le même type que m1 ;
``` cpp
std::cout << res << std::endl ;
```
En réalité, `decltype` est utilisable partout ou le compilateur attend un nom
de type, par exemple dans la définition d'un alias :
```c++
// Fichier auto_decltype_1.cpp
---
## Compilation du code
#include <iostream>
Boost.SIMD étant uniquement fait de fichiers d'entête, sa compilation est assez simple : incluez le chemin des fichiers d'entête de Boost.SIMD et de Boost, et l'option du compilateur permettant d'activer les instructions SIMD. Par exemple, pour `gcc` :
template< typename LHS, typename RHS >
void affiche_division( LHS lhs, RHS rhs )
{
typedef decltype(lhs/rhs) Resultat ;
Resultat res = lhs/rhs ;
std::cout<<res<<std::endl ;
}
int main()
{
affiche_division(3.,2) ;
}
```
g++ my_code.cpp -O3 -o my_code.exe -I/path/to/boostsimd/ -I/path/to/boost/ -msse2
Ou encore :
La sortie de notre premier exemple devrait ressembler à quelque chose comme ca :
```c++
// Fichier auto_decltype_2.cpp
{42,42,42,42}
#include <iostream>
Jetons un coup d'oeil à l'assembleur (par exemple en utilisant `objdump`) :
template< typename LHS, typename RHS >
auto division( LHS lhs, RHS rhs ) -> decltype(lhs/rhs)
{ return lhs/rhs ; }
int main()
{
std::cout<<division(3.,2)<<std::endl ;
}
```
movaps 0x300(%rip),%xmm0
addps 0x2e6(%rip),%xmm0
mulps 0x2ff(%rip),%xmm0
movaps %xmm0,(%rsp)
Dans l'exemple ci-dessus, nous n'aurions pas pu placer `decltype(lhs/rhs)`
à la place de `auto`, à la gauche du nom de fonction, car à ce stade
les arguments `lhs` et `rhs` ne sont pas encore connus. Il est donc
nécessaire d'utiliser la déclaration à droite du type de retour...
ou ne rien déclarer du tout, car le compilateur peut déduire le
type de retour à partir de l'instruction `return`.
Tout va bien, nous avons bien obtenu des instructions `*ps`, et le niveau d'abstraction introduit par `boost::simd::pack` n'a pas généré d'instructions supplémentaires.
---
## `decltype` n'est pas `auto`
Attention : pour conclure avec le mot-clef `decltype`, notons qu'
**il n'est pas complètement équivalent** à `auto`. En effet il prend
l'exact type de l'expression passée en argument, sans retirer aucun
`const` et aucun `&`.
## Comment traiter une collection de données façon SIMD ?
Si vous avez envie d'utiliser l'inférence de type, mais sans toucher
au type original d'aucune manière... utilisez `decltype(auto)` à la
place de `auto`.
---
## Afficher le type d'une variable
Observons un nouvel exemple, qui effectue le produit terme à terme d'une grande collection de valeurs :
Lorsqu'interviennent `auto` et `decltype`, hors des cas triviaux,
il peut devenir difficile de connaitre à coup sûr le type d'une
variable. Comment s'en assurer ?
Pour soutirer certaines informations à un compilateur C++, il est courant
de lui fournir une instruction volontairement erronée, écrite de telle sorte
que le message d'erreur contiennent l'information recherchée.
Ci-dessous, nous déclarons un patron de classe qui n'est jamais défini, ce qui permet
indirectement d'en savoir plus sur le type déduit par auto pour x et y :
```c++
// Fichier auto_td_1.cpp
template <typename T> ;
class TypeDisplayer ;
int main()
``` cpp
void produit( int n, const double * v1, const double * v2, double * res )
{
const int the_answer = 42 ;
auto x = the_answer ;
auto y = & the_answer ;
TypeDisplayer<decltype(x)> x_type ; // errors containing
TypeDisplayer<decltype(y)> y_type ; // x's and y's types
int i ;
for ( i=O ; i<n ; ++i )
{
res[i] = v1[i]*v2[i] ;
}
}
```
Par ailleurs, certains outils d'édition de code tentent de vous informer de
façon interactive sur les types associés aux utilisations de `auto`
dans votre code, avec plus ou moins de succès.
Vous pourriez être tentés d'utiliser `typeid` et `std::type_info::name`, mais ces fonctions
sont requises d'éliminer les références, les const, et présentent souvent leur résultat
sous une forme mutilée (mangled).
Si vous voulez mettre des instructions d'affichage de type dans votre code, la piste la plus
aboutie est sans doute la bibliothèque Boost TypeIndex.
Au lieu de traiter les éléments un par un, on peut les regrouper par blocs en utilisant les `pack` de Boost.SIMD. Pour charger les données et récupérer les résultats, on s'appuiera sur `load` et store `store` :
``` cpp
...
#include <boost/simd/include/functions/load.hpp>
#include <boost/simd/include/functions/store.hpp>
---
## Un risque d'utilisation excessive
using namespace boost ;
typedef simd::pack<double> pdouble ;
const int psize = pdouble::static_size ;
L'utilisation de l'inférence de type apporte beaucoup de bénéfices :
* une écriture simplifiée,
* un algorithme plus lisible,
* une protection contre les fautes de frappes dans les noms de types,
* une plus grande généricité, donc un remaniement et une évolution plus facile du code.
void produit( int n, const double * v1, const double * v2, double * res )
{
int i = 0 ;
while ( i < n )
{
// prepare packs
pdouble pv1 = simd::load<pdouble>(v1) ;
pdouble pv2 = simd::load<pdouble>(v2) ;
Devant le succès et l'efficacité d'`auto`, le comité C++ a cherché d'élargir autant que possible ses
usages possibles. Cependant, nous conclurons sur une mise en garde : tout à la joie de ne plus avoir à expliciter
tous les types, on peut couvrir son code d'`auto`, même lorsque le type d'une variable est parfaitement
connu, déterministe et rapide à taper. Ceci pour tendre vers un style de programmation ou tous les types
sont déduits par le compilateur.
// computation
pdouble pres = pv1*pv2 ;
Il faut être bien conscient que le code résultant peut devenir moins lisible, car il y est plus difficile
de visualiser le type d'une variable, donc de voir et corriger les erreurs de type. De plus, l'utilisation
excessive d'`auto` peut briser une abstraction, en amenant un compilateur à accepter un objet d'un type
incorrect dans l'initialisation d'une variable, ou comme paramètre d'une fonction, pour ne s'en plaindre
que plus tard (lorsque le code en fait un usage incohérent).
// store the result
simd::store<pdouble>(pres,res) ;
Enfin, le mot-clé `auto` peut apparaitre dans de nombreux contextes, chacun utilisant des règles
d'inférence subtilement différentes et complexes, par exemple lors de la combinaison avec l'initialisation
par accolades. Il est aisé de commettre une erreur dès que l'on sort du cas trivial d'une nouvelle
variable avec une valeur initiale.
// advance to the next pack
i += psize ;
v1 += psize ;
v2 += psize ;
res += psize ;
En toute chose, mesure est bonne...
}
}
```
---
## Jeu
## Exercice
Dans toutes les questions ci-dessous, nous vous invitons à deviner le type de la
variable `x`, ou bien le type `X`, et à vérifier votre réponse en utilisant la technique
de "Type Displayer" décrite ci-dessus.
Le fichier `simd.cpp` est une adaptation de notre exemple habituel de calculs complexes... réécrit sans complexes, qui ne sont pas encore pleinement supportés par Boost.SIMD. Modifiez le pour faire les calculs principaux grâce aux registres vectoriels du processeur. Testez de la façon habituelle :
Nous commencons par les cas triviaux et les usages simples et "sains". Nous
terminons par les cas complexes et les pièges, afin de vous invitez à la prudence
et à éviter un usage "excessif" de ce nouveau jouet.
**Question 1**
```c++
const int the_answer = 42 ;
auto x = the_answer ;
```
**Question 2**
```c++
const int the_answer = 42 ;
const int * px = &the_answer ;
auto x = px ;
```
./simd_make.sh simd 32768 50000
---
......@@ -393,7 +214,9 @@ Merci d'envoyer quelques commentaires à l'auteur :
---
## Sources
* [bSIMD](https://developer.numscale.com/bsimd/)
* [boost.simd @ GitHub](https://github.com/NumScale/boost.simd)
* [bSIMD @ NumScale](https://developer.numscale.com/bsimd/)
* [boost.simd @ Google Groups](https://groups.google.com/forum/#!forum/boost-simd)
---
......
FROM piscineri3/calculflottant
ADD . /work
WORKDIR /work
FROM gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculflottant
# Ensure use of bash
SHELL ["/bin/bash","-c"]
......@@ -17,6 +14,9 @@ RUN export BOOST_VERSION="1.62.0" && export BOOST_VERSION_=${BOOST_VERSION//./_}
ENV BOOST_ROOT /opt/boost
# bSIMD
RUN git clone https://github.com/NumScale/boost.simd.git -b v4.17.3.0 /opt/boost.simd
RUN git clone https://github.com/NumScale/boost.simd.git -b master /opt/boost.simd
ENV BOOST_SIMD_ROOT /opt/boost.simd
# Tuto files
ADD src /srcls
#!/bin/bash
docker build -t piscineri3/calculbsimd .
rm -rf src
cp -rf ../src src
docker build -t gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd .
rm -rf src
docker push gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd
#!/bin/bash
docker run -it --rm -v $PWD:/work -w /work piscineri3/calculbsimd
docker run -it --rm -w /src gitlab-registry.in2p3.fr/maitresnageurs/piscineji:calculbsimd /bin/bash
#!/bin/bash
g++ -std=c++14 auto_${1}_${2}.cpp -o auto_${1}_${2}.exe && ./auto_${1}_${2}.exe
\ No newline at end of file
#!/usr/bin/env sh
boost=${BOOST_ROOT}
boost_simd=${BOOST_SIMD_ROOT}
prog=${1}
shift
rm -f ${prog}.exe \
&& icpc -std=c++11 -mavx -wd1595 -I${BOOST_ROOT}/include -I${BOOST_SIMD_ROOT}/include ${prog}.cpp -o ${prog}.exe \
&& time ./${prog}.exe $*
#include <iostream>
#include <iomanip>
#include <cassert>
#include <cstdlib>
#include <cmath>
using namespace std ;
// generates a random complex number on the complex unit circle
void random_dcmplx( double & real, double & imag )
{
int r = rand() ;
double d = ((double) r)/RAND_MAX ;
double e = 2*M_PI*d ;
real = cos(e) ;
imag = sin(e) ;
}
// writes the array of n doubles in x
void write_numbers ( int n, double * real, double * imag )
{
for(int i=0; i<n; i++)
std::cout << scientific << std::setprecision(4)
<< "x[" << i << "] = ( " << real[i] << " , " << imag[i] << ")\n";
}
// global product, to check result
void global_product( int n, double * real, double * imag )
{
double zreal_tmp ;
double zreal = 1.0 ;
double zimag = 0.0 ;
for(int i=0; i<n; i++)
{
zreal_tmp = zreal*real[i] - zimag*imag[i] ;
zimag = zreal*imag[i] + zimag*real[i] ;
zreal = zreal_tmp ;
}
std::cout
<< scientific << std::setprecision(4)<<"x*... = ( "
<< zreal << " , " << zimag << ")\n" ;
}
// for arrays x and y of length n, on return y[i] equals x[i]**d
void compute_powers ( int n, double * xreal, double * ximag, double * yreal, double * yimag, int d )
{
for(int i=0; i < n; i++) // y[i] = pow(x[i],d); pow is too efficient
{
double rreal_tmp ;
double rreal = 1.0 ;
double rimag = 0.0 ;
for (int j=0; j < d; j++)
{
rreal_tmp = rreal*xreal[i] - rimag*ximag[i] ;
rimag = rreal*ximag[i] + rimag*xreal[i] ;
rreal = rreal_tmp ;
}
yreal[i] = rreal ;
yimag[i] = rimag ;
}
}
// programme principal
int main ( int argc, char * argv[] )
{
assert(argc==4) ;
int dim = atoi(argv[1]) ;
int degree = atoi(argv[2]) ;
int verbosity = atoi(argv[3]) ;
// prepare arrays
double * const inputr = new double [dim] ;
double * const inputi = new double [dim] ;
double * const outputr = new double [dim] ;
double * const outputi = new double [dim] ;
// generate input
srand(20120203) ; //srand(time(0));
for(int i=0; i<dim; i++)
random_dcmplx(inputr[i],inputi[i]) ;
if (verbosity)
{ write_numbers(dim,inputr,inputi) ; }
global_product(dim,inputr,inputi) ;
compute_powers(dim,inputr,inputi,outputr,outputi,degree) ;
if (verbosity)
{
std::cout<<"power : "<<degree<<std::endl ;
write_numbers(dim,outputr,outputi) ;
}
global_product(dim,outputr,outputi) ;
delete [] inputr ;
delete [] inputi ;
delete [] outputr ;
delete [] outputi ;
return 0 ;
}
#include <boost/simd/sdk/simd/pack.hpp>
#include <boost/simd/include/functions/load.hpp>
#include <boost/simd/include/functions/store.hpp>
#include <boost/simd/include/functions/plus.hpp>
#include <boost/simd/include/functions/multiplies.hpp>
#include <iostream>
#include <iomanip>
#include <cassert>
#include <cstdlib>
#include <cmath>
using namespace std ;
// generates a random complex number on the complex unit circle
void random_dcmplx( double & real, double & imag )
{