Hoje mesmo pensei em comparar o tempo de excução de uma função com o mesmo objetivo, mas escrita de duas formas diferentes. Queria ver se uma recursão ou um loop são mais rápidos de serem executados no R. Para isso, escrevi duas versões de uma função para calcular o n-ésimo termo da sequência de Fibonacci. Minha hipótese é que a recursão seria mais lenta do que um loop, mas eu gostaria de quantificar isso. Portanto, criei as funções fibo_recursiva
e fibo_loop
para testar a minha hipótese.
fibo_recursiva <- function(n){
if (n <= 2){
return(1)
} else {
return(fibo_recursiva(n-1) + fibo_recursiva(n-2))
}
}
fibo_loop <- function(n){
if (n <= 2){
return(1)
} else {
x <- c(1,1)
for (j in 3:n){
x[j] <- x[j-1] + x[j-2]
}
return(tail(x, 1))
}
}
Perceba que ambas informam o mesmo resultado numérico para os primeiros termos da sequência de Fibonacci:
fibo_recursiva(10)
## [1] 55
fibo_loop(10)
## [1] 55
fibo_recursiva(30)
## [1] 832040
fibo_loop(30)
## [1] 832040
Ao compará-las para estimar os tempos de processamento para os mesmo termos 10 e 30 da sequência, que que surge a minha surpresa
library(microbenchmark)
microbenchmark(fibo_recursiva(10),
fibo_loop(10),
unit = "ms")
## Unit: milliseconds
## expr min lq mean median uq max
## fibo_recursiva(10) 0.031898 0.032226 0.03251382 0.032472 0.032718 0.033989
## fibo_loop(10) 0.005617 0.005781 0.00608809 0.005904 0.006027 0.022181
## neval cld
## 100 a
## 100 b
microbenchmark(fibo_recursiva(30),
fibo_loop(30),
unit = "ms")
## Unit: milliseconds
## expr min lq mean median uq
## fibo_recursiva(30) 508.855961 517.8171055 523.04059980 518.989029 520.6031375
## fibo_loop(30) 0.011767 0.0129765 0.02967949 0.016236 0.0472935
## max neval cld
## 584.651513 100 a
## 0.070028 100 b
Para o cálculo do décimo elemento, a recursão levou, em média 0.03140067ms para calcular o décimo elemento da sequência, enquando o loop precisou de apenas 0.00616517ms, em média. Ou seja, é uma diferença de uma ordem de magnitude. Mas, ao calcular o trigésimo termo, a diferença aumenta muito. Foram 4 ordens de magnitude a mais (505.95837128ms versus 0.03198902ms).
Isso se deve à forma como uma função recursiva é executada. Para calcular o décimo termo, temos o seguinte:
F(10) = F(9) + F(8)
F(10) = F(8) + F(7) + F(7) + F(6)
F(10) = F(7) + F(6) + F(6) + F(5) + F(6) + F(5) + F(5) + F(4)
F(10) = ...
E isso vai pesando nos cálculos. Assim, a árvore de recursão vai crescendo muito, e calculando muitos termos que são desnecessários. Note como na terceira linha F(6)
e F(5)
são calculadas, cada uma, três vezes.
O livro R Inferno tem uma solução para isso chamada memoization. Ela consiste em salvar em cache resultados intermediários de uma função. Assim, quando o mesmo valor da função deveria ser recalculado, o resultado já está disponível e se economiza poder de processamento e, consequentemente, tempo.
O exemplo de memoização que o livro fornece é justamente utilizando a sequência de Fibonacci:
fibo_memoise <- local({
memory <- list()
function(n) {
valueName <- as.character(n)
if (!is.null(memory[[valueName]])) return(memory[[valueName]])
if (n == 0) return(0)
if (n == 1) return(1)
res <- Recall(n - 1) + Recall(n - 2)
memory[[valueName]] <<- res # store results
res
}
})
Rodando novamente nosso benchmark, o resultado obtido é muito bom:
microbenchmark(fibo_recursiva(10),
fibo_loop(10),
fibo_memoise(10),
unit = "ms")
## Unit: milliseconds
## expr min lq mean median uq max
## fibo_recursiva(10) 0.031898 0.0323900 0.03283116 0.032718 0.0331485 0.035055
## fibo_loop(10) 0.005617 0.0058425 0.00625701 0.006027 0.0062730 0.023329
## fibo_memoise(10) 0.002091 0.0021320 0.00312215 0.002296 0.0024190 0.083189
## neval cld
## 100 a
## 100 b
## 100 c
microbenchmark(fibo_recursiva(30),
fibo_loop(30),
fibo_memoise(30),
unit = "ms")
## Unit: milliseconds
## expr min lq mean median uq
## fibo_recursiva(30) 509.524917 516.5006570 522.75938326 518.6592045 520.0424625
## fibo_loop(30) 0.011767 0.0140425 0.03374669 0.0357520 0.0525620
## fibo_memoise(30) 0.002419 0.0030545 0.01452835 0.0132225 0.0221605
## max neval cld
## 580.541878 100 a
## 0.075030 100 b
## 0.161417 100 b
Dessa vez, a função fibo_memoise
foi mais rápida do que o loop, levando metade do tempo, em média. Ou seja, é um ganho impressionante de desempenho para a função recursiva.