Mostrar os Benefícios de C++ e quando usá-lo no seu código R
Geralmente as linguagens de programação são categorizadas entre linguagens dinamicamente tipadas ou estaticamente tipadas.
O R é uma linguagem dinamicamente tipada. Isso quer dizer que os tipos das variáveis e dados não são declarados no código e, portanto, conhecidos/checados somente em tempo de execução. Além do R, Ruby, Python e Clojure também são exemplos de linguagens dinamicamente tipadas. A principal vantagem de uma linguagem dinamicamente tipada é a sua agilidade: você se torna mais produtivo e o código mais enxuto, mas isto vem com um custo. A principal desvantagem é que linguagems dinamicamente tipadas são bem mais lentas em tempo de execução.
O C++ é uma linguagem estaticamente tipada. Isso quer dizer que os tipos das variáveis e dos dados são explicitamente definidos no código e, portanto, conhecidos/checados em tempo de compilação. Além de C++, Java, C#, F#, Kotlin e Go são exemplos de linguagens estaticamente tipadas. A principal vantagem de uma linguagem estaticamente tipada é a sua velocidade em tempo de execução. Sua principal desvantagem é a rigidez: você se torna menos produtivo e o código muito mais verboso.
Não conseguimos definir um índice preciso de comparação de tempo de execução entre R, C++ ou qualquer outra linguagem de programação. Isto varia muito conforme aplicação, sistema operacional e tamanho dos dados.
Como primeiro exemplo, imagine uma função que soma três números inteiros e retorna o valor da soma. Vamos chamar essa função de add()
e compararemos o tempo de execução dessa função em R addR()
e em C++ addCpp()
:
addR <- function(x, y, z) {
result <- x + y + z
return(result)
}
addR(10, 17, 31)
[1] 58
library("Rcpp")
cppFunction("
int addCpp(int x, int y, int z){
int result = x + y + z;
return result;
}")
addCpp(10, 17, 31)
[1] 58
Comparando tempo de execução com a função mark()
do pacote {bench}
:
b1 <- bench::mark(
R = addR(10, 17, 31),
Cpp = addCpp(10, 17, 31),
relative = TRUE
)
b1
# A tibble: 2 x 6
expression min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 R 1 1 4.24 131. NaN
2 Cpp 3.07 4.90 1 1 NaN
Aqui vocês vem que C++ não tem nenhuma vantagem sobre R: é mais verboso, chato de escrever e ainda é mais lenta!
Como segundo exemplo vamos fazer algo mais computacionalmente intensivo. Para quem não conhece, Amostrador de Gibbs (Gibss Sampler) é um algoritmo de aproximação de uma distribuição probabilística multivariada que usa Método de Montecarlo com correntes Markov; e é primariamente utilizado em casos quando amostragem direta não é possível. Este exemplo foi retirado do blog do Dirk Eddelbuettel mantenedor do ecossistema {Rcpp}
de pacotes de interfaces entre R e C++.
O código em R é assim:
Agora o código em C++ (não se preocupe agora com os Rcpp
que aparecem no código, isto será explicado na segunda parte desse tutorial):
sourceCpp(code =
"#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericMatrix gibbsCpp(int N, int thin) {
NumericMatrix mat(N, 2);
double x = 0, y = 0;
for(int i = 0; i < N; i++) {
for(int j = 0; j < thin; j++) {
x = rgamma(1, 3, 1 / (y * y + 4))[0];
y = rnorm(1, 1 / (x + 1), 1 / sqrt(2 * (x + 1)))[0];
}
mat(i, 0) = x;
mat(i, 1) = y;
}
return(mat);
}"
)
E vamos para o benchmark comparando alguns tamanhos de inputs:
b2 <- bench::press(
N = 10^c(2:3),
{
bench::mark(
R = gibbsR(N, 10),
Cpp = gibbsCpp(N, 10),
check = FALSE,
relative = TRUE
)
}
)
b2
# A tibble: 4 x 7
expression N min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 R 100 22.3 22.3 1 1284. 1.66
2 Cpp 100 1 1 22.9 1 1
3 R 1000 23.6 24.4 1 2745. 1.53
4 Cpp 1000 1 1 23.1 1 1
No meu computador gibbsCpp()
executa 20x mais rápido que gibbsR()
!
O foco aqui não é ensinar C++. Para isso recomendo você encontrar materiais e cursos que se conformem com o seu estilo de aprendizagem. Eu aprendi com um misto de quatro coisas:
A linguagem C++ é uma linguagem de programação orientada a objetos e é uma combinação de linguagem de baixo e alto nível – uma linguagem de nível médio. A linguagem de programação foi criada, projetada e desenvolvida por um cientista da computação dinamarquês – Bjarne Stroustrup da Bell Telephone Laboratories (agora conhecida como Nokia Bell Labs) em Murray Hill, New Jersey. Como ele queria uma linguagem flexível e dinâmica que fosse semelhante a C com todos os seus recursos, mas com adicionalidade de verificação de tipo ativa, herança básica, argumento de funcionamento padrão, classes, inlining, etc. e, portanto, C com Classes (C++) foi lançado .
C++ era inicialmente conhecido como “C com classes” e foi renomeado C ++ em 1983. ++ é uma abreviação para adicionar um à variedade na programação; portanto, C++ significa aproximadamente que “um maior que C.” Em 1998, foi criado o comitê de padrões C++ que publicou o primeiro padrão internacional ISO para C++, que seria informalmente conhecido como C++98. A cada três anos o comitê de padrões de C++ publica um novo padrão internacional e esses adquirem o sufixo do ano. Até agora temos C++11, C++14, C++17, C++20 e propostas para serem incluídas no C++23.
O padrão C++11 é importante, tanto que foi seu nome oficial é “o novo padrão C++”. A principal inovação é a biblioteca padrão de C++ (Standard Template Library – STL) que traz quatro componentes chamados algoritmos (algorithms), contêineres (containers), funções (functions) e iteradores (iterators). Os componentes, além de serem revolucionários (C++ pode ser considerado uma linguagem totalmente diferente depois do STL de C++11), são extremamente bem mantidos e documentados. A presença de bugs é quase nula fazendo com que todos os componentes sejam confiáveis e sempre funcionem da maneira que eles foram supostamente projetados para funcionar (a sonda da NASA em Marte possui seu software todo em C++).
Os principais tipos de variáveis de R e sua correspondência em C++ na tabela abaixo. Note que os nomes são bem similares.
R | C++ |
---|---|
|
|
|
|
|
|
|
|
|
|
Além disso, toda linha (instruções e declarações) de C++ deve ser encerrada com um ponto-e-vírgula ;
.
const
C++ tem diversos tipos de modificadores e qualificadores de variáveis (ex: long
, unsigned
, volatile
, static
, etc.). Não vou cobrir todos eles, mas apenas um: const
.
const
significa constante (constant) e é um qualificador usado quando você declara explicitamente que a variável não mudará o seu valor (por mais paradoxal que isso soe…) durante toda a execução do código. Por exemplo se eu tenho um int
chamado n_execucao
que não mudará o seu valor:
const int n_execucao = 10;
&
Cada variável que é criada em C++ (e na maioria das outras linguagems de programação) possui um endereço de memória que indica aonde a variável está localizada fisicamente na memória do computador. Esse endereço é como se fosse um código postal indicando aonde conseguimos encontrar a variável. Para criar uma referência usamos o símbolo &
. Isto cria uma variável que é uma referência à outra variável e o valor de uma variável referência é o endereço da variável da qual ela referencia. Veja alguns exemplos (obs: //
é como comentamos código em C++, equivalente ao #
em R):
int i = 5; // variável int
int& j = i; // variável j que é uma referência a i
// endereço de memória da variável int i
&i; // incrementar i em 1
i++; // TRUE
i == j; 5; // FALSE j agora possui o mesmo valor que i (6) j ==
Por que usar referências? Por dois motivos:
A figura 4 mostra a anatomia de uma função em C++. Ela é bem similar à estrutura de funções no R. Com duas notórias diferenças:
return
. No R podemos ser mais desleixados pois o valor de retorno será sempre a ultima declaração da função. Em C++ isto não funciona. A lógica de C++ é que a função termina quando ela atinge o primeiro return
e retorna o dado/variável especificado(a).Às vezes é mais eficiente especificar um parâmetro como uma referência do que como um valor. Veja o caso abaixo de uma função simples de incremento de número inteiro. No primeiro caso, a função increment_val()
usa o valor do parâmetro x
. Isto implica na função gerar uma cópia de x
no escopo local da função. Já no segundo caso, a função increment_ref()
usa a referência do parâmetro x
. Isto não implica em cópia de x
, pois a função manipula o parâmetro x
no escopo global.
Como podem ver, nesse cenário simples temos um pequeno ganho, pelo tempo mediano de execução, ao usarmos o a referência como parâmetro. Claro que nesse exemplo a função é trivial, imaginem como essa diferença escalonaria para maior intensidade computacional ou maior entrada de dados. Sem contar que precisamos de ZERO coletor de lixo (garbage collection – gc
), pois não estamos movendo/copiando nada para o escopo local da função.
cppFunction(
"int increment_val(int x){
x++;
return x;
}"
)
cppFunction(
"int increment_ref(int& x){
x++;
return x;
}"
)
bench::mark(
value = increment_val(5),
reference = increment_ref(5)
)
# A tibble: 2 x 6
expression min median `itr/sec` mem_alloc `gc/sec`
<bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
1 value 1.42µs 1.61µs 509325. 2.49KB 50.9
2 reference 1.4µs 1.58µs 589542. 2.49KB 0
for
em C++Obs: Em C++ todos os índices são baseados em zero. Ou seja, o primeiro elemento de uma
array
éarray[0]
.
Os loops for
de C++ são similares ao de R. Eles seguem a seguinte síntaxe:
for (inicialização; condição; incremento) instrução;
Funciona da seguinte maneira:
inicialização
é executada. Geralmente, isso declara uma variável de contador (counter) e a define para algum valor inicial. Isso é executado uma única vez, no início do loop.condição
é verificada. Se for verdade, o loop continua; caso contrário, o loop termina e a instrução
é ignorada, indo diretamente para a etapa 5.instrução
é executada. Como de costume, pode ser uma única instrução ou um bloco entre chaves {}
.incremento
é executado e o loop volta para a etapa 2.Veja um exemplo4:
for (int i=0; i < 5; i++) {
"\n";
cout << i << }
C++11 introduziu uma funcionalidade chamada de range-for
loop para ser usado com principalmente com os contêineres da C++11 STL, simplificando muito mais a síntaxe:
for (declaração : expressão)
instrução
declaração
- define uma variável. Deve ser possível converter cada elemento da sequência para o tipo da variável. A maneira mais fácil de garantir que os tipos correspondam é usar o especificador de tipo auto
.expressão
- deve representar uma sequência, como uma lista de inicializadores entre chaves, um array
ou um objeto como umvector
ou string
que tem membros begin
e end
que retornam iteradores.Exemplo:
int> a {1, 2, 3, 4, 5};
vector<
for (auto i: a) {
cout << i << endl; }
Compare com o for
loop tradicional (muito mais verboso):
int> a {1, 2, 3, 4, 5};
vector<
for(int i=0; i < a.size(); i++){
cout << a[i] << endl; }
while
em C++Loops while
em C++ são quase que idênticos aos do R. Veja um exemplo:
int i = 0;
while (i < 10){
i++;"\n";
cout << i << }
if
, else if
e else
em C++Desvios condicionais em C++ são também quase que idênticos aos do R. Veja um exemplo:
int i = 2;
if (i > 2) {
" é maior que 2\n";
cout << i << else if (i == 2){
} " é extamente igual a 2\n";
cout << i << else {
} " é menor que 2\n";
cout << i << }
switch
Os casos condicionais usando o operador switch
em C++ é também muito similar ao R. A única diferença é que ao invés de =
temos :
para especificar os casos, veja a síntaxe:
switch(i) {
case 1 : cout << '1'; // imprime "1"
case 2 : cout << '2'; // imprime "2"
default : cout << "default\n"; // imprime "default"
}
Os gargalos típicos que C++ pode resolver incluem:
Esses contextos são todos os cenários que C++ pode ajudar o seu código. “Nem mesmo os mais sábios sabem o fim de todos os caminhos”5.
Nesse breve sobrevoo de C++ a intenção é apenas possibilitar alguém que não tenha nenhum conhecimento em C++ à ser introduzido em alguns conceitos de C++, notoriamente focando em funções (eu acredito ser a principal razão de usarmos C++ em R – acelerar o tempo de execução de funções). Usando uma analogia, esse sobrevoo permite o leitor “dirigir C++ por um estacionamento vazio”. Caso se interesse, C++ é uma linguagem EXTREMAMENTE rica e complexa. Veja os livros, referências, vídeos e podcasts que eu recomendei lá em cima.
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] dplyr_1.0.5 gt_0.2.2 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 yaml_2.2.1
[10] utf8_1.1.4 rlang_0.4.10 jquerylib_0.1.3
[13] pillar_1.5.1 glue_1.4.2 DBI_1.1.1
[16] jpeg_0.1-8.1 lifecycle_1.0.0 stringr_1.4.0
[19] commonmark_1.7 munsell_0.5.0 gtable_0.3.0
[22] ragg_1.1.1 bench_1.1.1 evaluate_0.14
[25] knitr_1.31 parallel_4.0.4 fansi_0.4.2
[28] profmem_0.6.0 highr_0.8 backports_1.2.1
[31] checkmate_2.0.0 scales_1.1.1 debugme_1.1.0
[34] jsonlite_1.7.2 farver_2.1.0 systemfonts_1.0.1
[37] textshaping_0.3.2 distill_1.2 png_0.1-7
[40] ggplot2_3.3.3 digest_0.6.27 stringi_1.5.3
[43] grid_4.0.4 cli_2.3.1 tools_4.0.4
[46] magrittr_2.0.1 sass_0.3.1 tibble_3.1.0
[49] crayon_1.4.1 tidyr_1.1.3 pkgconfig_2.0.3
[52] downlit_0.2.1 ellipsis_0.3.1 assertthat_0.2.1
[55] rmarkdown_2.7 rstudioapi_0.13 R6_2.5.0
[58] compiler_4.0.4
Julia é um caso a parte pois pode ser tanto codificada de maneira estática quando dinâmica, mas isso é assunto para outro tutorial.↩︎
um dos hosts do podcast ADSP.↩︎
Algoritmos + Estruturas de Dados = Programas.↩︎
aqui estamos usando o cout
do <iostream>
. Explicarei como printar mensagens no R usando C++ na segunda parte desse tutorial↩︎
um dos meus objetivos era incluir no mínimo uma referência de Star Wars (já feita no Home) e uma de Senhor dos Anéis↩︎
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++: Por que C++? R não é suficiente?. Retrieved from https://storopoli.github.io/Rcpp/1-Porque_CPP.html
BibTeX citation
@misc{storopoli2021porquecpp, author = {Storopoli, Jose}, title = {Rcpp - A interface entre R e C++: Por que C++? R não é suficiente?}, url = {https://storopoli.github.io/Rcpp/1-Porque_CPP.html}, year = {2021} }