Easy::jit é uma biblioteca assistida por compilador que permite a geração de código Just-In-Time simples para códigos C++.
Primeiro, instale o clang e o LLVM.
apt install llvm-6.0-dev llvm-6.0-tools clang-6.0
Em seguida, configure e compile o projeto.
cmake -DLLVM_DIR=/usr/lib/llvm-6.0/cmake < path_to_easy_jit_src >
cmake --build .
Para construir os exemplos, instale a biblioteca opencv e adicione os sinalizadores -DEASY_JIT_EXAMPLE=1
ao comando cmake.
Para ativar o benchmarking, instale a estrutura de benchmark do Google e adicione os sinalizadores -DEASY_JIT_BENCHMARK=1 -DBENCHMARK_DIR=<path_to_google_benchmark_install>
ao comando cmake.
Tudo está pronto para começar!
Se você quiser fazer apenas um teste rápido do projeto, tudo está fornecido para utilizá-lo com o docker. Para fazer isso, gere um Dockerfile a partir do diretório atual usando os scripts em <path_to_easy_jit_src>/misc/docker
e, em seguida, gere sua instância do 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
Como a biblioteca Easy::Jit depende do auxílio do compilador, é obrigatório carregar um plugin do compilador para utilizá-la. A bandeira -Xclang -load -Xclang <path_to_easy_jit_build>/bin/EasyJitPass.so
carrega o plugin.
Os cabeçalhos incluídos requerem suporte C++14 e lembre-se de adicionar os diretórios include! Use --std=c++14 -I<path_to_easy_jit_src>/cpplib/include
.
Finalmente, o binário deve ser vinculado à biblioteca de tempo de execução Easy::Jit, usando -L<path_to_easy_jit_build>/bin -lEasyJitRuntime
.
Juntando tudo obtemos o comando abaixo.
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
Considere o código abaixo de um software que aplica filtros de imagem em um stream de vídeo. Nas seções a seguir iremos adaptá-lo para usar a biblioteca Easy::jit. A função de otimização é kernel
, que aplica uma máscara em toda a imagem.
A máscara, suas dimensões e área não mudam com frequência, portanto, especializar a função para esses parâmetros parece razoável. Além disso, as dimensões da imagem e o número de canais normalmente permanecem constantes durante toda a execução; entretanto, é impossível saber seus valores, pois dependem do fluxo.
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 ());
}
O cabeçalho principal da biblioteca é easy/jit.h
, onde a única função principal da biblioteca é exportada. Esta função é chamada - adivinhe como? --fácil easy::jit
. Adicionamos a diretiva include correspondente no topo do arquivo.
# include < easy/jit.h >
Com a chamada para easy::jit
, especializamos a função e obtemos uma nova levando apenas dois parâmetros (o quadro de entrada e o quadro de saída).
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 incorpora a representação de código de bits LLVM das funções para se especializar em tempo de execução no código binário. Para fazer isso, a biblioteca requer acesso à implementação dessas funções. Easy::jit faz um esforço para deduzir quais funções são especializadas em tempo de execução, mas em muitos casos isso não é possível.
Neste caso, é possível utilizar a macro EASY_JIT_EXPOSE
, conforme mostrado no código a seguir,
void EASY_JIT_EXPOSE kernel () { /* ... */ }
ou usando uma expressão regular durante a compilação. O comando abaixo exporta todas as funções cujo nome começa com "^kernel".
clang++ ... -mllvm -easy-export= " ^kernel.* " ...
Paralelamente ao cabeçalho easy/jit.h
, existe easy/code_cache.h
que fornece um cache de código para evitar a recompilação de funções que já foram geradas.
Abaixo mostramos o código da seção anterior, mas adaptado para usar um cache de código.
# 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 ));
}
Veja o arquivo LICENSE
no diretório de nível superior deste projeto.
Agradecimentos especiais à Quarkslab pelo apoio no trabalho em projetos pessoais.
Serge Guelton (serge_sans_paille)
Juan Manuel Martinez Caamaño (jmmartinez)
Kavon Farvardin (kavon) autor de atJIT