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

Por Caio em 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

Nossa Newsletter

Uma vez por semana enviamos um e-mail para você não perder nenhum post da Curso-R. Avisamos também sempre que abrimos uma nova turma.