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:

N = 10

##    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))

Utilizando a função microbenchmark

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.