Faremos aqui um exemplo de como comparar o desempenho de funções no R. Representaremos por meio de gráficos os resultados obtidos para cada um dos métodos empregados. A tarefa a ser desempenhada consiste na soma dos elementos das colunas de uma matriz. Os métodos empregados estão implementados como segue. Ao final, são geradas 10 matrizes de um mihão de elementos e 1000 linhas, para aplicar os métodos. O desempenho de cada método é guardado no data frame Experiments.
N = 10 # Número de experimentos
Experiments <- as.data.frame(matrix(NA, nrow = N, ncol=5))
colnames(Experiments) <- c("Doubleloop", "Singleloop", "Apply", "ColSum", "MatrixRowSum")
# Função 1: Doubleloop
Doubleloop <- function(){
Time <- system.time({
colsums <- rep(NA, dim(big.matrix)[2])
for(i in 1:dim(big.matrix)[2]){
s <- 0
for(j in 1:dim(big.matrix)[1])
s <- s + big.matrix[j,i]
}
colsums[i] <- s
})[1]
return (Time)
}
# Função 2: Singleloop
Singleloop <- function(){
Time <- system.time({
colsums <- rep(NA, dim(big.matrix)[2])
for(i in 1:dim(big.matrix)[2]){
colsums[i] <- sum(big.matrix[,i])
}
})[1]
return (Time)
}
# Função 3: Apply
Apply <- function(){
Time <- system.time({
colsums <- apply(big.matrix, 2, sum)
})[1]
return (Time)
}
# Função 4: ColSum
ColSum <- function(){
Time <- system.time({
colsums <- colMeans(big.matrix)
})[1]
return (Time)
}
# Função 5: MatrixRowSum
MatrixRowSum <- function(){
Time <- system.time({
colsums <- rep(1, 1000) %*% big.matrix
})[1]
return (Time)
}
for (i in 1:N){
big.matrix <- matrix(rnorm(1e+6), nrow=1000) # criando a matriz
Experiments[i,] <- c(Doubleloop(), Singleloop(), Apply(), ColSum(), MatrixRowSum())
cat("Interaction = ", i, "of ", N, "...\n")
}
cat("This is the end...")
Apresentação dos resultados:
## Doubleloop Singleloop Apply ColSum MatrixRowSum
## 1 1.00 0.01 0.03 0.00 0.02
## 2 1.04 0.02 0.02 0.00 0.00
## 3 1.11 0.05 0.03 0.00 0.02
## 4 1.14 0.03 0.03 0.00 0.02
## 5 1.16 0.02 0.04 0.00 0.00
## 6 1.40 0.07 0.05 0.00 0.01
## 7 1.10 0.03 0.05 0.01 0.00
## 8 0.99 0.03 0.03 0.00 0.00
## 9 1.06 0.03 0.03 0.00 0.00
## 10 1.06 0.03 0.03 0.01 0.00
Agora, utilizamos as bibliotecas dplyr, tidyr e ggplot2 para rearrumar a tabela, adicionar filtros às variáveis e construir gráficos:
suppressPackageStartupMessages(library(dplyr)) # Omite a mensagem inicial do pacote
library("tidyr")
library("ggplot2")
p <- Experiments %>% gather( method, Time)%>%filter(method != "DoubleLoop") %>%
ggplot(aes(x = method, y= Time))
Utilizamos o código abaixo para gerar um boxplot. Nota-se que o método que usa um duplo for para percorrer a matriz é o mais ineficiente.
p + geom_boxplot(aes(fill = method), alpha=0.5) + geom_jitter(aes(col=method))
Reproduziremos o mesmo código, no entanto, ao invés de utilizar system.time, lançaremos mão da função microbenchmark. Modificamos as funções para que elas retornem a soma das colunas. Os resultados do desempenho são guardados no objeto performance. Os resultados de performance foram dispostos em milissegundos.
# Função 1: Doubleloop
Doubleloop <- function(){
colsums <- rep(NA, dim(big.matrix)[2])
for(i in 1:dim(big.matrix)[2]){
s <- 0
for(j in 1:dim(big.matrix)[1])
s <- s + big.matrix[j,i]
colsums[i] <- s
}
return (colsums)
}
# Função 2: Singleloop
Singleloop <- function(){
colsums <- rep(NA, dim(big.matrix)[2])
for(i in 1:dim(big.matrix)[2]){
colsums[i] <- sum(big.matrix[,i])
}
return (colsums)
}
# Função 3: Apply
Apply <- function(){
colsums <- apply(big.matrix, 2, sum)
return (colsums)
}
# Função 4: ColSum
ColSum <- function(){
colsums <- colMeans(big.matrix)
return (colsums)
}
# Função 5: MatrixRowSum
MatrixRowSum <- function(){
colsums <- rep(1, 1000) %*% big.matrix
return (colsums)
}
Agora que as funções foram modificadas para retornar a soma das colunas de uma matriz, chama-se a função microbenchmark para cada uma delas, gerando uma tabela onde é possível fazer as comparações.
set.seed(123)
big.matrix <- matrix(rnorm(1e+6), nrow=1000) # Gerando uma única matriz
library("microbenchmark")
# Analisando a performance em milissegundos:
performance <- microbenchmark(
DOUBLELOOP = Doubleloop(),
SINGLELOOP = Singleloop(),
APPLY = Apply(),
COLSUM = ColSum(),
MATRIXROWSUM = MatrixRowSum(),
times = 100L
)
options(microbenchmark.unit="ms") # Resultados em milissegundos
Uma dificuldade encontrada foi: como fazer isso para diferentes matrizes? Ao preencher os parâmetros da função microbenchmark, pode-se arbitrar a quantidade de repetições, ou seja, testa-se as funções 1000 vezes, mas com a mesma matriz. Seria interessante encontrar uma forma de fazer com que cada replicação ocorresse com uma matriz diferente. Seguem os resultados, um boxplot e uma representação gráfica do desempenho:
performance # Imprimindo os resuldados
## Unit: milliseconds
## expr min lq mean median uq
## DOUBLELOOP 972.825634 1050.411331 1104.333958 1088.558873 1132.456145
## SINGLELOOP 18.411219 20.079576 24.034993 21.268122 24.238207
## APPLY 25.360043 45.591856 58.648939 49.407808 60.700003
## COLSUM 1.741340 1.767853 1.819951 1.778401 1.837986
## MATRIXROWSUM 8.530512 8.606061 9.923961 8.751459 9.758117
## max neval
## 1404.399078 100
## 106.377387 100
## 121.947952 100
## 2.562974 100
## 35.313732 100
# Gráfico utilizando uma função do pacote ggplot2
qplot(y = time, data = performance, colour=expr) + scale_y_log10()
resultados <- as.data.frame(performance)
p <- resultados %>% ggplot(aes(x = expr, y= time))
# Para retirar, use filter(expr != "DOUBLELOOP")
# Fazendo um boxplot
p + geom_boxplot(aes(fill = expr), alpha = .5) + geom_jitter(aes(col=expr))
Percebemos pelo boxplot que a função que faz o duplo for para percorrer a matriz é a mais ineficiente, enquanto aquela que usa o comando colSums é a mais eficiente. Tal resultado está de acordo com o obtido pela função system.time. No entanto, a função microbenchmark possui uma precisão maior, o que torna possível observar mais claramente que há diferenças de desempenho entre as funções.