Fazendo o R Voar: uma Introdução ao Rcpp

Por Caio 23/11/2017

O que fazer quando precisamos que o nosso script rode mais rápido? Geralmente a primeira ideia que temos é otimizar o código: reduzir a quantidade de laços, diminuir o tamanho das estruturas, utilizar programação paralela, etc… Mas quando se trata de R, temos a possibilidade de aumentar a velocidade do código sem alterar praticamente nada da sua estrutura.

Neste post darei uma introdução básica ao pacote Rcpp, uma ferramenta que nos permite rodar código em C++ de dentro do R.

O básico

C++ é uma linguagem de programação muito famosa e versátil. Ela têm elementos de programação genérica, imperativa e orientada a objeto e também fornece uma interface para manipulação de memória de baixo nível.

Uma característica interessante do C++ é que ele é extremamente veloz. Diferentemente do R, ela é uma linguagem compilada, com tipagem estática e que não fornece tantas abstrações de operações, permitindo que seu código execute com eficiência incrível.

Para explorar os benefícios que o C++ pode trazer para o seu código R, instale e carregue o pacote Rcpp com os comandos abaixo:

install.packages("Rcpp")
library(Rcpp)

Vejamos agora um exemplo simples de como chamar código C++ do R. O jeito mais fácil de fazer isso é através da função cppFunction(): ela recebe uma string que será interpretada como uma função em C++.

adicao_r <- function(x, y, z) {
  sum = x + y + z
  return(sum)
}

cppFunction(
  "int adicao_c(int x, int y, int z) {
    int sum = x + y + z;
    return sum;
  }")

adicao_r(1, 2, 3)
#> [1] 6

adicao_c(1, 2, 3)
#> [1] 6

Como podemos observar no exemplo acima, ambas as funções têm o mesmo comportamento apesar de algumas diferenças superficiais. Note como temos sempre que declarar os tipos das variáveis em C++! Usando a palavra-chave int deixamos claro para o compilador que uma variável terá o tipo inteiro e até mesmo que uma função deve retornar um valor de tipo inteiro. Outra coisa que é importante lembrar é que precisamos colocar um ponto-e-vírgula após cada comando C++.

Vetores

Normalmente o C++ teria diferenças enormes em relação ao R no seu tratamento de vetores, mas para a nossa sorte o Rcpp nos disponibiliza uma biblioteca de estruturas que abstraem o comportamento do R. No exemplo a seguir temos uma função que recebe um número e vetor numérico, computa a distância euclidiana entre o valor e o vetor e retorna um vetor numérico como saída.

dist_r <- function(x, ys) {
  sqrt((x - ys) ^ 2)
}

cppFunction(
  "NumericVector dist_c(double x, NumericVector ys) {
    int n = ys.size();
    NumericVector out(n);

    for(int i = 0; i < n; i++) {
      out[i] = sqrt(pow(ys[i] - x, 2.0));
    }
    return out;
  }")

dist_r(10, 20:25)
#> [1] 10 11 12 13 14 15

dist_c(10, 20:25)
#> [1] 10 11 12 13 14 15

A estrutura NumericVector abstrai um vetor numérico do R, nos permitindo trabalhar com ele de uma maneira mais familiar. Com o método .size() obtemos o seu comprimento (equivalente a length()) e podemos declarar um novo com o construtor NumericVector nome(comprimento);. O único ponto de diferença fundamental entre o C++ e o R é que o primeiro não possui operações vetorizadas propriamente ditas, fazendo com que precisemos usar laços para toda e qualquer iteração.

Velocidade máxima

Certos aspectos da filosofia do R o tornam uma linguagem extremamente versátil, mas isso vem com certas desvantagens. Alguns pontos em que a performance do R deixa a desejar são laço não vetorizáveis (por uma iteração depender da anterior), funções recursivas e estruturas de dados complexas.

Nestas e em muitas outras situações, usar C++ pode ser extremamente vantajoso. No exemplo a seguir veremos a diferença entre a performance de um laço em C++ e um em R; note que esta nem é uma das 3 situações listadas no parágrafo anterior e que mesmo assim o código em C++ é 6 vezes mais rápido.

soma_r <- function(v) {
  total <- 0
  for (e in v) {
    if (e < 0) { total = total - e }
    else if (e > 0.75) { total = total + e/2 }
    else { total = total + e }
  }

  return(total)
}

cppFunction(
  "double soma_c(NumericVector v) {
    double total = 0;
    for (int i = 0; i < v.size(); i++) {
      if (v[i] < 0) { total -= v[i]; }
      else if (v[i] > 0.75) { total += v[i]/2; }
      else { total += v[i]; }
    }

    return(total);
  }")

v <- runif(100000, -1, 1)
microbenchmark::microbenchmark(soma_r(v), soma_c(v))
#> Unit: milliseconds
#>       expr      min       lq     mean   median       uq       max neval
#>  soma_r(v) 6.105048 6.436608 6.911819 6.718456 7.183266 11.610624   100
#>  soma_c(v) 1.045805 1.063956 1.161585 1.097920 1.210052  1.955702   100

Obs.: Os símbolos += e -= são equivalentes a a = a +/- b, já o símbolo ++ é equivalente a a = a + 1.

Conclusão

Com o pacote Rcpp, podemos rodar código em C++ de dentro do próprio R. Através dessa técnica conseguimos otimizar nosso código ou mesmo ter acesso a estruturas de dados complexas disponibilizadas pelo C++.

Para saber mais sobre o assunto, dê uma olhada no tutorial escrito por Hadley Wickham no livro Advanced R. Também recomendo a própria página do Rcpp e sua extensa galeria de exemplos.

P.S.: Se você quiser o código completo deste tutorial, disponibilizei ele em um Gist. Além disso, também escrevi uma versão em inglês deste post no meu blog pessoal. Abraços!

comments powered by Disqus