Como fazer seu código Rcpp ser ainda mais rápido
Vimos que o {Rcpp}
faz o seu código R ficar muito mais rápido. Tudo o que mostramos até agora foi usando apenas um único core/processador (single thread) do computador1. Agora imaginem o quão rápido seu código R pode ficar se você conseguir rodar {Rcpp}
em paralelo 🤯!
TBB
O Pacote {RcppParallel}
usa a biblioteca TBB
da Intel. TBB
(Threading Building Blocks) é uma biblioteca de C++ desenvolvida pela Intel para programação paralela em processadores multi-core. Usando TBB
, um cálculo é dividido em tarefas que podem ser executadas em paralelo. A biblioteca gerencia e agenda threads para executar essas tarefas.
{Rcpp}
em paralelo – {RcppParallel}
Primeiro, certifique-se que você possui a biblioteca TBB
da Intel instalada:
sudo apt install libtbb-dev
brew install tbb
Segundo, instale o pacote {RcppParallel}
para R.
Terceiro, coloque em todo código que deseja paralelizar com {RcppParallel}
a seguinte síntaxe:
#include <Rcpp.h>
#include <RcppParallel.h>
using namespace Rcpp;
using namespace RcppParallel;
//[[Rcpp::depends(RcppParallel)]]
Pronto! É isso.
{RcppParallel}
É possível implementar paralelização em diversas partes do seu código {Rcpp}
com o {RcppParallel}
. Aqui eu vou cobrir apenas os dois algoritmos paralelos do {RcppParallel}
2:
parallelFor
: Este aqui é fácil de explicar. Qualquer loop for
do seu código pode ser um bom candidato à paralelização.parallelReduce
: Reduce
é um algoritmo bem conhecido em ciências da computação. Reduce
aplica um operação binária (como adição) em uma sequência definida de elementos, resultando em um único valor. O exemplo sum_of_squares
do tutorial 2. Como incorporar C++ no R - {Rcpp} é uma aplicação de um Reduce
3. Toda vez que você tiver essa situação você pode paralelizar com parallelReduce
.Ambos os algortimos usam o struct
Worker
definido no código do {RcppParallel}
que é uma interface para a biblioteca TBB
.
Além disso {RcppParallel}
usa duas classes, uma para vetores e outra para matrizes:
RVector<T>
– onde T
é o tipo de variável (double
, int
etc.)RMatrix<T>
– onde T
é o tipo de variável (double
, int
etc.)Ao carregar o {RcppParallel}
é importante você designar o número de threads/cores que deseja que o código {RcppParallel}
use para paralelização. Caso queira usar todos os seus threads/cores disponíveis, coloque como argumento parallel::detectCores()
que retorna um número inteiro com todos os threads/cores disponíveis no seu computador. Aqui estou usando todos os threads/cores disponíveis: 12 threads/cores.
Se não especificado, por padrão {RcppParallel}
usa todos os threads/cores disponíveis.
library(Rcpp)
library(RcppParallel)
setThreadOptions(parallel::detectCores())
print(parallel::detectCores())
[1] 12
parallelFor
– Paralelizando Loops for
Para usar o parallelFor
você deve criar um objeto Worker
e definir um operador operator()
desse objeto que será invocado pelo {RcppParallel}
e roda em paralelo. Isso cria uma função com com o nome do objeto Worker
que você criou4. Essa função toma como argumento um intervalo [começo, fim)
e lida com todas as questões de segurança e travas de threads que são um porre bem complicadas de maneira automática. Note que o elemento fim
do intervalo não é incluído no intervalo (mesmo padrão de comportamento dos iteradores end
da biblioteca padrão C++11 STL).
Para mais detalhes, consulte a documentação do parallelFor
no site do {RcppParallel}
.
parallelFor
– Raiz Quadrada de Elementos da Matriz.Aqui vou usar o exemplo da documentação do parallelFor
no site do {RcppParallel}
de uma função paralela que cacula a raiz quadrada dos elementos de uma matriz. Adicionei alguns comentários para você entender o que está sendo feito. Além disso, há uma versão single-thread também que vamos testar desempenho.
#include <Rcpp.h>
#include <RcppParallel.h>
#include <algorithm>
using namespace Rcpp;
using namespace RcppParallel;
// [[Rcpp::depends(RcppParallel)]]
// Criando um objeto Worker chamado SquareRoot
struct SquareRoot : public Worker
{// Variáveis Membro públicas
const RMatrix<double> input;
double> output;
RMatrix<
// Construtor do Objeto Worker SquareRoot
const Rcpp::NumericMatrix input, Rcpp::NumericMatrix output)
SquareRoot(
: input(input), output(output) {}
// Overload do operador () -- functor
void operator()(std::size_t begin, std::size_t end) {
std::transform(input.begin() + begin,
input.begin() + end,
output.begin() + begin,
::sqrt);
}
};
// Função que chama o Objeto Worker SquareRoot
// [[Rcpp::export]]
NumericMatrix parallelMatrixSqrt(NumericMatrix x) {
// Variável local output inicializada
NumericMatrix output(x.nrow(), x.ncol());
// Invocação do operador() do Objeto Worker SquareRoot
SquareRoot squareRoot(x, output);
// Paralelização do loop for
0, x.length(), squareRoot);
parallelFor(
return output;
}
// Versão single-thread
// [[Rcpp::export]]
NumericMatrix matrixSqrt(NumericMatrix orig) {
NumericMatrix mat(orig.nrow(), orig.ncol());std::transform(orig.begin(), orig.end(), mat.begin(), ::sqrt);
return mat;
}
set.seed(123)
b1 <- bench::press(
n = 10^c(2:3),
{
X = matrix(rnorm(n * n), nrow = n)
bench::mark(
Rcpp = matrixSqrt(X),
RcppParallel = parallelMatrixSqrt(X),
check = FALSE,
relative = TRUE
)
})
b1
# A tibble: 4 x 7
expression n min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Rcpp 100 3.14 3.33 1 1 1
2 RcppParallel 100 1 1 3.01 1 2.88
3 Rcpp 1000 2.36 2.90 1 1 1
4 RcppParallel 1000 1 1 2.78 1 2.83
Um ganho de 3x com paralelização.
parallelReduce
– Paralelizando operações Reduce
Reduce
é um algoritmo bem conhecido em ciências da computação. Reduce
aplica um operação binária (como adição) em uma sequência definida de elementos, resultando em um único valor. O exemplo sum_of_squares
do tutorial 2. Como incorporar C++ no R - {Rcpp} é uma aplicação de um Reduce
5. Toda vez que você tiver essa situação você pode paralelizar com parallelReduce
.
A lógica do parallelReduce
é similar ao parallelFor
. Primeiro ambos usam objetos Worker
, com algumas diferenças:
Worker
: um padrão e um “divisor”. O construtor padrão pega os dados de entrada e inicializa qualquer valor que está sendo acumulado (por exemplo, inicializar uma soma para zero). O construtor de divisão é chamado quando o trabalho precisa ser dividido em outros threads - ele toma uma referência à instância da qual está sendo dividido e simplesmente copia o ponteiro para os dados de entrada e inicializa seu valor “acumulado” para zero.operator()
que executa o trabalho. Isso funciona da mesma forma que o operador operator()
em parallelFor
, mas em vez de gravar em outro vetor ou matriz, ele normalmente acumula um valor.Para mais detalhes, consulte a documentação do parallelReduce
no site do {RcppParallel}
parallelReduce
– Soma dos QuadradosVamos reutilizar o exemplo sum_of_squares
do tutorial 2. Como incorporar C++ no R - {Rcpp}.
Soma dos quadrados é algo que ocorre bastante em computação científica, especialmente quando estamos falando de regressão, mínimos quadrados, ANOVA etc. Vamos paralelizar a implementação ingênua que fizemos no tutorial 2. Como incorporar C++ no R - {Rcpp} com dois loops for
. Lembrando que esta implementação será uma função que aceita como parâmetro um vetor de números reais (C++ double
/ R numeric
) e computa a soma de todos os elementos do vetor elevados ao quadrado.
Aqui vamos inserir um std::accumulate()
do header <numeric>
.
Novamente vou incluir comentários para o entendimento do que estamos fazendo no {RcppParallel}
. Além disso, há uma versão single-thread também que vamos testar desempenho.
#include <Rcpp.h>
#include <RcppParallel.h>
#include <algorithm>
using namespace RcppParallel;
using namespace Rcpp;
// [[Rcpp::depends(RcppParallel)]]
// [[Rcpp::plugins("cpp11")]]
// [[Rcpp::plugins("cpp2a")]]
// Criando um objeto Worker chamado sum_of_squares
struct sum_of_squares : public Worker
{// Variáveis Membro públicas
const RVector<double> input;
double value;
// Construtor padrão do Objeto Worker
const NumericVector input) : input(input), value(0) {}
sum_of_squares(
// Construtor "divisor"
const sum_of_squares& sum, Split) : input(sum.input), value(0) {}
sum_of_squares(
// Overload do operador ()
void operator()(std::size_t begin, std::size_t end) {
std::accumulate(input.begin() + begin,
value +=
input.begin() + end,0.0,
auto i, auto j) {return i + (j * j);});
[] (
}
void join(const sum_of_squares& rhs) {
value += rhs.value;
}
};
// Função que chama o Objeto Worker sum_of_squares
// [[Rcpp::export]]
double parallel_sum_of_squares(NumericVector x) {
// variável local inicializada
sum_of_squares sum(x);
// Paralelização do Reduce
0, x.length(), sum);
parallelReduce(
return sum.value;
}
// Versão single-thread
// [[Rcpp::export]]
double sum_of_squares(NumericVector x) {
return std::accumulate(x.begin(),
x.end(),0.0,
auto i, auto j) {return i + (j * j);});
[] ( }
b2 <- bench::press(
n = 10^c(4:6),
{
v = rnorm(n)
bench::mark(
Rcpp = sum_of_squares(v),
RcppParallel = parallel_sum_of_squares(v),
check = FALSE,
relative = TRUE
)
})
b2
# A tibble: 6 x 7
expression n min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 Rcpp 10000 1.27 1 1.13 1 NaN
2 RcppParallel 10000 1 1.15 1 1 Inf
3 Rcpp 100000 4.82 3.84 1 1 NaN
4 RcppParallel 100000 1 1 3.81 1 Inf
5 Rcpp 1000000 8.96 8.13 1 1 NaN
6 RcppParallel 1000000 1 1 8.34 1 NaN
Mais um sucesso! Ganho de 8x 🤯 com paralelização.
{RcppParallel}
no seu Pacote RAs instruções abaixo foram retiradas da documentação do {RcppParallel}
.
Se você deseja usar {RcppParallel}
de dentro de um pacote R, você precisa editar vários arquivos para criar os links de construção e tempo de execução necessários. As seguintes adições devem ser feitas:
No DESCRIPTION
:
Imports: RcppParallel
LinkingTo: RcppParallel
SystemRequirements: GNU make
No NAMESPACE
:
importFrom(RcppParallel, RcppParallelLibs)
No src\Makevars
:
CXX_STD = CXX11
PKG_LIBS += $(shell ${R_HOME}/bin/Rscript -e "RcppParallel::RcppParallelLibs()")
No src\Makevars.win
:
CXX_STD = CXX11
PKG_CXXFLAGS += -DRCPP_PARALLEL_USE_TBB=1
PKG_LIBS += $(shell "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" \
-e "RcppParallel::RcppParallelLibs()")
R version 4.0.4 (2021-02-15)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 20.10
Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0
locale:
[1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
[3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
[5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C
[9] LC_ADDRESS=C LC_TELEPHONE=C
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
attached base packages:
[1] stats graphics grDevices utils datasets methods
[7] base
other attached packages:
[1] RcppParallel_5.0.3 Rcpp_1.0.6
loaded via a namespace (and not attached):
[1] tidyselect_1.1.0 xfun_0.22 bslib_0.2.4
[4] purrr_0.3.4 colorspace_2.0-0 vctrs_0.3.6
[7] generics_0.1.0 htmltools_0.5.1.1 emo_0.0.0.9000
[10] yaml_2.2.1 utf8_1.1.4 rlang_0.4.10
[13] jquerylib_0.1.3 pillar_1.5.1 glue_1.4.2
[16] DBI_1.1.1 lifecycle_1.0.0 stringr_1.4.0
[19] munsell_0.5.0 gtable_0.3.0 ragg_1.1.1
[22] bench_1.1.1 evaluate_0.14 knitr_1.31
[25] parallel_4.0.4 fansi_0.4.2 profmem_0.6.0
[28] highr_0.8 scales_1.1.1 jsonlite_1.7.2
[31] debugme_1.1.0 farver_2.1.0 systemfonts_1.0.1
[34] textshaping_0.3.2 distill_1.2 ggplot2_3.3.3
[37] digest_0.6.27 stringi_1.5.3 dplyr_1.0.5
[40] grid_4.0.4 cli_2.3.1 tools_4.0.4
[43] magrittr_2.0.1 sass_0.3.1 tibble_3.1.0
[46] tidyr_1.1.3 crayon_1.4.1 pkgconfig_2.0.3
[49] downlit_0.2.1 ellipsis_0.3.1 lubridate_1.7.10
[52] assertthat_0.2.1 rmarkdown_2.7 rstudioapi_0.13
[55] R6_2.5.0 compiler_4.0.4
tecnicamente, Eigen
e Armadillo
podem, dependendo da configuração do sistema operacional, automaticamente se beneficiar de paralelizações usando o OpenMP
.↩︎
a biblioteca TBB
tem muito mais algoritmos complexos caso necessario. Recomendo você olhar este link da documentação do {RcppParallel}
.↩︎
tecnicamente é um MapReduce
.↩︎
mais questões técnicas: quando você define um operador operator()
de um objeto em C++ você dá um overload no operador parenthesis do objeto e o resultado é uma síntaxe similar à uma função com o nome do objeto.↩︎
tecnicamente é um MapReduce
.↩︎
If you see mistakes or want to suggest changes, please create an issue on the source repository.
Text and figures are licensed under Creative Commons Attribution CC BY-SA 4.0. Source code is available at https://github.com/storopoli/Rcpp, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".
For attribution, please cite this work as
Storopoli (2021, Feb. 2). Rcpp - A interface entre R e C++: Paralelização -- `{RcppParallel}`. Retrieved from https://storopoli.github.io/Rcpp/4-RcppParallel.html
BibTeX citation
@misc{storopoli2021rcppparallel, author = {Storopoli, Jose}, title = {Rcpp - A interface entre R e C++: Paralelização -- `{RcppParallel}`}, url = {https://storopoli.github.io/Rcpp/4-RcppParallel.html}, year = {2021} }