Paralelização no R

Programadores eficientes não precisam escrever algoritmos que rodam rápido. Recomendo fortemente a leitura do livro Efficient R, que discute eficiência com o R de forma exaustiva. Também gosto muito da primeira parte dessa palestra do Hadley, onde ele defende que o cientista de dados deve usar seu tempo pensando no problema e não na forma que vai escrever seu código.

Com isso em mente, vamos investigar o tema paralelização. Quando rodamos coisas em paralelo, mandamos os núcleos de processamento da máquina calcularem coisas diferentes ao mesmo tempo. A vantagem disso é que o tempo de execução dos algoritmos é dividido pelo número de núcleos disponíveis, sem exigir grandes mudanças no código utilizado.

Vamos mostrar como paralelizar um código usando a função llply() do pacote plyr. Essa função funciona de forma idêntica ao lapply(), ou seja, recebe uma lista ou vetor como input, aplica uma função em cada elemento, e retorna os resultados numa lista com o mesmo comprimento.

A função dormir() manda o R esperar seg segundos antes de concluir, retornando seg.

dormir <- function(seg = 1) {
  Sys.sleep(seg)
  return(seg)
}

É intuitivo afirmar que o tempo de execução de dormir() é compatível com seg.

system.time({
  dormir()
})
##    user  system elapsed 
##   0.000   0.000   1.005

Nosso interesse é aplicar dormir() em cada elemento do vetor c(1, 2). Esse algoritmo demora 1 + 2 = 3 segundos.

segundos <- c(1, 2)
system.time({
  plyr::llply(segundos, dormir)
})
##    user  system elapsed 
##   0.054   0.005   3.071

Agora vamos executar o mesmo código usando paralelização. Antes, precisamos

  1. criar e registrar as cópias de R que rodam em paralelo; e
  2. adicionar o parâmetro .parallel = TRUE no llply()

O primeiro passo é resolvido com os pacotes parallel e doParallel. Veja como fica o código:

cl <- parallel::makePSOCKcluster(2) # cria as cópias do R que rodam em paralelo
doParallel::registerDoParallel(cl)  # registra as cópias do R para serem usadas no plyr

system.time({
  plyr::llply(segundos, dormir, .parallel = TRUE)
})
##    user  system elapsed 
##   0.012   0.000   2.067

O tempo total de execução foi de ~2.2 segundos, um pouco mais do que dormir(2). Os dois décimos de segundo adicionais são necessários para preparar o terreno da paralelização. Inclusive, se você rodar o código em paralelo novamente, o tempo adicional cai para quase nada:

system.time({
  plyr::llply(segundos, dormir, .parallel = TRUE)
})
##    user  system elapsed 
##   0.005   0.001   2.011

Se quiser parar de rodar coisas em paralelo, basta rodar stopCluster():

parallel::stopCluster(cl) # para de rodar coisas em paralelo

E é isso, caros errantes. Rappy coding :)

comments powered by Disqus