Por que C++? R não é suficiente?

Mostrar os Benefícios de C++ e quando usá-lo no seu código R

Jose Storopoli https://scholar.google.com/citations?user=xGU7H1QAAAAJ&hl=en (UNINOVE)https://www.uninove.br
February 2, 2021

Linguagens Dinâmicas vs Estáticas1

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.

O quão lento R é comparado à C++?

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.

Exemplo - Simples Função de Soma

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
Benchmark da função soma: R vs C++

Figure 1: Benchmark da função soma: R vs C++

Aqui vocês vem que C++ não tem nenhuma vantagem sobre R: é mais verboso, chato de escrever e ainda é mais lenta!

Exemplo - Amostrador de Gibbs

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:

gibbsR <- function(N, thin) {
  mat <- matrix(nrow = N, ncol = 2)
  x <- y <- 0

  for (i in 1:N) {
    for (j in 1:thin) {
      x <- rgamma(1, 3, y * y + 4)
      y <- rnorm(1, 1 / (x + 1), 1 / sqrt(2 * (x + 1)))
    }
    mat[i, ] <- c(x, y)
  }
  return(mat)
}

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   
Benchmarks do Amostrador de Gibbs: R vs C++

Figure 2: Benchmarks do Amostrador de Gibbs: R vs C++

No meu computador gibbsCpp() executa 20x mais rápido que gibbsR()!

Sobrevoo de C++

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:

  1. Livros:
    • A Tour of C++: livro de 2018 do criador de C++, Bjarne Stroustrup. É bem conciso e recomendado para quem conhece alguma linguagem de programação orientada a objetos (ex: Python, Java, …).
    • C++ Primer: uma das principais referências de C++. Está na quinta edição de 2011. É interessante para aprender o básico de C++ junto com uma introdução à biblioteca padrão (Standard Template Library – STL). Como C++ lança uma nova edição a cada 3 anos, esse livro não possui todas as novas funcionalidades de C++14, C++17 e C++20.
  2. Referências:
    • cppreference.com: aqui está a bíblia de C++ totalmente atualizada. Para tudo o que você precisar vale a pena consultar aqui, mas ela é bem técnica. Necessário um certo nível de conhecimento básico em C++ para compreender.
    • geeksforgeeks.com: Geeks for Geeks é um site da Índia sobre programação com uma abordagem mais descontraída. A referência de C++ é bem mais “leve” que a cppreference. Vale a pena para ajudar a entender conceitos que você não tenha entendido pois tem exemplos interessantes.
  3. Vídeos no YouTube:
    • CppCon: a principal conferência anual sobre C++, todos os vídeos das sessões da conferência são disponibilizados gratuitamente no YouTube.
    • CppNow: outra conferência anual sobre C++ com uma pegada mais descontraída e participação da platéia. O vídeo do Conor Hoekstra2 sobre Algorithm Intution é fenomenal.
    • Meeting Cpp: outra conferência anual sobre C++, possui bons vídeos com uma pegada similar a CppNow.
    • The Cherno: Um ex-programador de jogos da EA. Ele tem uma didática excelente e eu recomendo a playlist C++ do seu canal.
  4. Podcasts:
    • CppCast: podcast de entrevista com desenvolvedores e pessoas de destaque da comunidade C++. Sempre iniciam os episódios comentando com o entrevistado as últimas notícias do mundo C++ e programação em geral.
    • cpp.chat: similar ao CppCast.
    • ADSP: este é um podcast novo e promissor. Seu nome ADSP significa Algorithms + Data Structures = Programs3 e um dos hosts é o Conor Hoekstra programador da NVIDIA e o palestrante do vídeo Algorithm Intution.

História do C++

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 .

Bjarne Stroustrup. Figura de: www.wikipedia.org

Figure 3: Bjarne Stroustrup. Figura de: www.wikipedia.org

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++).

Tipos de Variáveis – R vs 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++

logical

bool

integer

int

numeric

double

complex

complex

character

string

Além disso, toda linha (instruções e declarações) de C++ deve ser encerrada com um ponto-e-vírgula ;.

O qualificador – 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;

Referências – &

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
&i;         // endereço de memória da variável int i
i++;        // incrementar i em 1
i == j;     // TRUE
j == 5;     // FALSE j agora possui o mesmo valor que i (6)

Por que usar referências? Por dois motivos:

  1. Para evitar a cópia de objetos na memória: isso faz o código ficar mais eficiente.
  2. Paramêtros de funções podem especificados por valor ou por referências. No caso de referências, isso é extremamente útil quando queremos alterar os valores dos parâmetros in-place pela função.

Funções em C++

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:

Anatomia de uma função em C++. Figura adaptada da Vinheta Oficial Introdutória do `{Rcpp}` de Dirk Eddelbuettel

Figure 4: Anatomia de uma função em C++. Figura adaptada da Vinheta Oficial Introdutória do {Rcpp} de Dirk Eddelbuettel

Usando valores vs referências como parâmetros

À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 collectiongc), 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  

Loops 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:

  1. A 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.
  2. 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.
  3. instrução é executada. Como de costume, pode ser uma única instrução ou um bloco entre chaves {}.
  4. incremento é executado e o loop volta para a etapa 2.
  5. o loop termina: a execução continua na próxima instrução após ela.

Veja um exemplo4:

for (int i=0; i < 5; i++) {
      cout << i << "\n";
    }

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

Exemplo:

vector<int> a {1, 2, 3, 4, 5};

for (auto i: a) {
  cout << i << endl;
}

Compare com o for loop tradicional (muito mais verboso):

vector<int> a {1, 2, 3, 4, 5};

for(int i=0; i < a.size(); i++){
   cout << a[i] << endl;
}

Loops 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++;
    cout << i << "\n";
}

Desvios condicionais 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) {
    cout << i << " é maior que 2\n";
} else if (i == 2){
    cout << i << " é extamente igual a 2\n";
} else {
    cout << i << " é menor que 2\n";
}

Casos condicionais 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"
}

Quando usar C++ no seu código R?

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.

O que mais existe em C++?

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.

Ambiente

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   

  1. Julia é um caso a parte pois pode ser tanto codificada de maneira estática quando dinâmica, mas isso é assunto para outro tutorial.↩︎

  2. um dos hosts do podcast ADSP.↩︎

  3. Algoritmos + Estruturas de Dados = Programas.↩︎

  4. aqui estamos usando o cout do <iostream>. Explicarei como printar mensagens no R usando C++ na segunda parte desse tutorial↩︎

  5. 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↩︎

Corrections

If you see mistakes or want to suggest changes, please create an issue on the source repository.

Reuse

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 ...".

Citation

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}
}