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.031775 0.032144 0.03238918 0.032267 0.032554 0.037884
## fibo_loop(10) 0.005576 0.005740 0.00615082 0.005822 0.005986 0.030463
## 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) 509.120698 515.519855 521.34994184 516.9512675 518.4165050
## fibo_loop(30) 0.011849 0.014309 0.03540186 0.0201925 0.0565185
## max neval cld
## 585.327890 100 a
## 0.068716 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.031857 0.032431 0.03283731 0.032800 0.033169 0.035260
## fibo_loop(10) 0.005617 0.005863 0.00627710 0.006068 0.006232 0.024149
## fibo_memoise(10) 0.002091 0.002173 0.00308484 0.002296 0.002419 0.080442
## 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) 510.783453 516.0568115 521.88163189 517.4019395 518.6915125
## fibo_loop(30) 0.011972 0.0142270 0.03133015 0.0170355 0.0523160
## fibo_memoise(30) 0.002460 0.0031775 0.01727945 0.0200080 0.0284335
## max neval cld
## 584.970698 100 a
## 0.063099 100 b
## 0.178637 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.