Easy::jit est une bibliothèque assistée par un compilateur qui permet une génération simple de code juste à temps pour les codes C++.
Tout d’abord, installez Clang et LLVM.
apt install llvm-6.0-dev llvm-6.0-tools clang-6.0
Ensuite, configurez et compilez le projet.
cmake -DLLVM_DIR=/usr/lib/llvm-6.0/cmake < path_to_easy_jit_src >
cmake --build .
Pour créer les exemples, installez la bibliothèque opencv et ajoutez les indicateurs -DEASY_JIT_EXAMPLE=1
à la commande cmake.
Pour activer l'analyse comparative, installez le framework de référence Google et ajoutez les indicateurs -DEASY_JIT_BENCHMARK=1 -DBENCHMARK_DIR=<path_to_google_benchmark_install>
à la commande cmake.
Tout est prêt à partir !
Si vous souhaitez tester rapidement le projet, tout est fourni pour l'utiliser avec docker. Pour ce faire, générez un Dockerfile à partir du répertoire actuel à l'aide des scripts dans <path_to_easy_jit_src>/misc/docker
, puis générez votre instance Docker.
python3 < path_to_easy_jit_src > /misc/docker/GenDockerfile.py < path_to_easy_jit_src > /.travis.yml > Dockerfile
docker build -t easy/test -f Dockerfile
docker run -ti easy/test /bin/bash
Puisque la bibliothèque Easy::Jit repose sur l'assistance du compilateur, il est obligatoire de charger un plugin de compilateur pour pouvoir l'utiliser. Le drapeau -Xclang -load -Xclang <path_to_easy_jit_build>/bin/EasyJitPass.so
charge le plugin.
Les en-têtes inclus nécessitent le support de C++14, et n'oubliez pas d'ajouter les répertoires d'inclusion ! Utilisez --std=c++14 -I<path_to_easy_jit_src>/cpplib/include
.
Enfin, le binaire doit être lié à la bibliothèque d'exécution Easy::Jit, en utilisant -L<path_to_easy_jit_build>/bin -lEasyJitRuntime
.
En rassemblant le tout, nous obtenons la commande ci-dessous.
clang++-6.0 --std=c++14 < my_file.cpp >
-Xclang -load -Xclang /path/to/easy/jit/build/bin/bin/EasyJitPass.so
-I < path_to_easy_jit_src > /cpplib/include
-L < path_to_easy_jit_build > /bin -lEasyJitRuntime
Considérez le code ci-dessous provenant d'un logiciel qui applique des filtres d'image sur un flux vidéo. Dans les sections suivantes, nous allons l'adapter pour utiliser la bibliothèque Easy::jit. La fonction à optimiser est kernel
, qui applique un masque sur toute l'image.
Le masque, ses dimensions et sa surface ne changent pas souvent, il semble donc raisonnable de spécialiser la fonction pour ces paramètres. De plus, les dimensions de l'image et le nombre de canaux restent généralement constants pendant toute l'exécution ; cependant, il est impossible de connaître leurs valeurs car elles dépendent du flux.
static void kernel ( const char * mask, unsigned mask_size, unsigned mask_area,
const unsigned char * in, unsigned char * out,
unsigned rows, unsigned cols, unsigned channels) {
unsigned mask_middle = (mask_size/ 2 + 1 );
unsigned middle = (cols+ 1 )*mask_middle;
for ( unsigned i = 0 ; i != rows-mask_size; ++i) {
for ( unsigned j = 0 ; j != cols-mask_size; ++j) {
for ( unsigned ch = 0 ; ch != channels; ++ch) {
long out_val = 0 ;
for ( unsigned ii = 0 ; ii != mask_size; ++ii) {
for ( unsigned jj = 0 ; jj != mask_size; ++jj) {
out_val += mask[ii*mask_size+jj] * in[((i+ii)*cols+j+jj)*channels+ch];
}
}
out[(i*cols+j+middle)*channels+ch] = out_val / mask_area;
}
}
}
}
static void apply_filter ( const char *mask, unsigned mask_size, unsigned mask_area, cv::Mat &image, cv::Mat *&out) {
kernel (mask, mask_size, mask_area, image. ptr ( 0 , 0 ), out-> ptr ( 0 , 0 ), image. rows , image. cols , image. channels ());
}
L'en-tête principal de la bibliothèque est easy/jit.h
, où la seule fonction principale de la bibliothèque est exportée. Cette fonction s'appelle -- devinez comment ? -- easy::jit
. Nous ajoutons la directive include correspondante en haut du fichier.
# include < easy/jit.h >
Avec l'appel à easy::jit
, on spécialise la fonction et en obtenons une nouvelle ne prenant que deux paramètres (la frame d'entrée et la frame de sortie).
static void apply_filter ( const char *mask, unsigned mask_size, unsigned mask_area, cv::Mat &image, cv::Mat *&out) {
using namespace std ::placeholders ;
auto kernel_opt = easy::jit (kernel, mask, mask_size, mask_area, _1, _2, image. rows , image. cols , image. channels ());
kernel_opt (image. ptr ( 0 , 0 ), out-> ptr ( 0 , 0 ));
}
Easy::jit intègre la représentation bitcode LLVM des fonctions à spécialiser au moment de l'exécution dans le code binaire. Pour ce faire, la bibliothèque nécessite l'accès à l'implémentation de ces fonctions. Easy::jit fait un effort pour déduire quelles fonctions sont spécialisées au moment de l'exécution, mais dans de nombreux cas, cela n'est pas possible.
Dans ce cas, il est possible d'utiliser la macro EASY_JIT_EXPOSE
, comme indiqué dans le code suivant,
void EASY_JIT_EXPOSE kernel () { /* ... */ }
ou en utilisant une expression régulière lors de la compilation. La commande ci-dessous exporte toutes les fonctions dont le nom commence par "^kernel".
clang++ ... -mllvm -easy-export= " ^kernel.* " ...
En parallèle de l'en-tête easy/jit.h
, il existe easy/code_cache.h
qui fournit un cache de code pour éviter la recompilation de fonctions déjà générées.
Ci-dessous, nous montrons le code de la section précédente, mais adapté pour utiliser un cache de code.
# include < easy/code_cache.h >
static void apply_filter ( const char *mask, unsigned mask_size, unsigned mask_area, cv::Mat &image, cv::Mat *&out) {
using namespace std ::placeholders ;
static easy::Cache<> cache;
auto const &kernel_opt = cache. jit (kernel, mask, mask_size, mask_area, _1, _2, image. rows , image. cols , image. channels ());
kernel_opt (image. ptr ( 0 , 0 ), out-> ptr ( 0 , 0 ));
}
Voir le fichier LICENSE
dans le répertoire de niveau supérieur de ce projet.
Un merci spécial à Quarkslab pour son soutien dans le travail sur des projets personnels.
Serge Guelton (serge_sans_paille)
Juan Manuel Martinez Caamaño (jmmartinez)
Kavon Farvardin (kavon) auteur d'atJIT