Computação paralela é um tema complexo e vasto, mas ainda pouco conhecido. Graças ao esforço da comunidade de R já foram criados diversos pacotes que tornam a paralelização de códigos muito mais fácil e segura. Neste texto abordamos alguns exemplos práticos e introdutórios para demonstrar os ganhos dessa abordagem.
O objetivo é apresentar instrumentos e demonstrar o ganho imediato de paralelizar o seu código de R. Para detalhes mais técnicos e uma discussão mais aprofundada do tema recomendamos o capítulo 4 do livro Data Science for Economists and Other Animals de McDermott & Rubin.
Os pacotes utilizados podem ser instalados/carregados conforme o código abaixo:
if(!require("pacman")) install.packages("pacman") pacman::p_load( "tictoc", "purrr", "future", "parallelly", "furrr" )
Exemplo: raiz quadrada super lenta
Nosso exemplo prático vai ser o cálculo de uma raiz quadrada realizado de forma super lenta. Para isso criaremos a função abaixo que calcula a raiz quadrada de um número mas em uma versão "lenta", ou seja, obriga o computador a esperar 2 segundos antes de fazer qualquer coisa. Isso servirá apenas para fins didáticos de demonstração da paralelização que faremos à seguir:
raiz_lenta <- function(x) { Sys.sleep(2) x_sqrt <- sqrt(x) return(x_sqrt) }
Execução serial
Agora vamos usar essa função criada iterando uma sequência de números de forma serial, ou seja, os cálculos não serão executados em paralelo. Utilizaremos aqui abordagem do pacote purrr do tidyverse para iterações no R. A função map_dbl() retorna um vetor numérico com os resultados dos cálculos. Para contabilizar o tempo de execução do código utilizaremos o pacote tictoc:
tictoc::tic() exemplo_serial <- purrr::map_dbl(1:10, raiz_lenta) tictoc::toc(log = TRUE) ## 20.25 sec elapsed
Conforme esperado, a iteração durou 20 segundos para ser concluída, pois forçamos uma parada de 2 segundos na função raiz_lenta() a cada iteração serial. Isso significa que podemos facilmente acelerar esse código se rodarmos a iteração em paralelo.
Execução paralela
Para rodar o código em paralelo primeiro é importante saber que o procedimento depende do número de núcleos de processador disponíveis no computador. Uma maneira fácil de obter essa informação no R é através do código abaixo:
parallelly::availableCores() ## system ## 4
Em segundo lugar, é preciso configurar o R para executar o código em paralelo, e é neste ponto que o pacote future, criado pelo Henrik Bengtsson, entra em cena.
Neste exemplo sugerimos a utilização do pacote future pois o mesmo oferece uma sintaxe simples e intuitiva para quem já está acostumado com o tidyverse. Graças a este pacote, o único procedimento que devemos fazer é especificar uma estratégia, ou seja, executar o código de maneira serial, paralela, etc., através da função plan(). Como queremos executar em paralelo, especificamos a estratégia multisession() que cria várias sessões do R no computador para executar o código.
Geralmente recomenda-se alocar para utilização em paralelo a quantidade total de núcleos de processador deixando um livre para outras tarefas do computador - como, por exemplo, navegação na internet, editor de texto, etc. -, mas aqui utilizaremos 2 núcleos apenas. Para outras opções consulte a documentação da função multisession().
future::plan( strategy = future::multisession(workers = 2) )
Realizada a configuração do R para execução em paralelo, vamos voltar ao nosso exemplo de iteração do cálculo de raiz quadrada. Executaremos novamente essa iteração, mas dessa vez em paralelo. Para isso faremos uma modificação marginal no código que é utilizar uma variante da função purrr::map_dbl() específica para iteração em paralelo, que é a função furrr::future_map_dbl().
tictoc::tic() exemplo_paralelo <- furrr::future_map_dbl(1:10, raiz_lenta) tictoc::toc(log = TRUE) ## 10.58 sec elapsed
Um resultado empolgante: o tempo de execução caiu pela metade! Isso tudo com pouquíssimas alterações em nosso código, bastou configurar o R (através do future::plan) e alterar a função de iteração (furrr::future_map_dbl). Um ganho de performance fenomenal, não?!
Vamos verificar se o output é idêntico nos dois casos (serial e paralelo):
all.equal(exemplo_serial, exemplo_paralelo) ## [1] TRUE
Agora você já pode iniciar um olhar mais crítico sobre os seus códigos para identificar pontos de melhoria de performance.
_______________________
(*) Para entender mais sobre a linguagem R e suas ferramentas, confira nosso Curso de Introdução ao R para análise de dados.