Estamos interessados em analisar o desempenho de funções que façam multiplicação matricial, para isso iremos comparar diversas funções implementadas em R e em C. A seguir definiremos as funções. Estamos interessados na que seja mais rápida, exigindo menos tempo computacional.
Utilizaremos as funções implementadas em R como vemos a seguir
m_0 <- function(M, x){
return(M %*% x)
}
m_1 <- function(M, x){
Mx <- matrix(NA, nrow = n, ncol = 1)
for(i in 1:nrow(M)){
Mx[i,1] = M[i,] %*% x
}
return(Mx)
}
m_2 <- function(M, x){
Mx <- matrix(NA, nrow = n, ncol = 1)
for(i in 1:nrow(M)){
val = 0
for (j in 1:ncol(M)){
val = val + M[i,j]* x[j,1]
}
Mx[i,1] = val
}
return(Mx)
}
m_3 <- function(M, x){
Mx = apply(M, 1, function(m){m %*% x})
return(matrix(Mx,ncol = 1))
}
Utilizaremos as funções em C implementadas a seguir
#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]
#include <omp.h>
// [[Rcpp::export]]
arma::vec c_0(const arma::mat& M, const arma::vec& x){
return(M*x);
}
// [[Rcpp::export]]
Rcpp::NumericVector c_1(Rcpp::NumericMatrix M, Rcpp::NumericVector x)
{
int r = M.nrow();
int c = M.ncol();
Rcpp::NumericVector Mx(r);
for (int i = 0; i < r; i++)
{
double val = 0;
for (int j = 0; j < c; j++)
{
val += M(i,j)*x(j);
}
Mx(i) = val;
}
return(Mx);
}
// [[Rcpp::export]]
arma::vec c_2(const arma::mat M, const arma::vec x)
{
arma::vec Mx(x.size(), arma::fill::zeros);
int r = M.n_rows;
int c = M.n_cols;
for (int i = 0; i < r; i++)
{
double val = 0;
for (int j = 0; j < c; j++)
{
val += M(i,j)*x(j);
}
Mx(i) = val;
}
return(Mx);
}
// [[Rcpp::export]]
arma::vec c_2inmp(const arma::mat M, const arma::vec x)
{
arma::vec Mx(x.size(), arma::fill::zeros);
int r = M.n_rows;
int c = M.n_cols;
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < r; i++)
{
double val = 0;
for (int j = 0; j < c; j++)
{
val += M(i,j)*x(j);
}
Mx(i) = val;
}
return(Mx);
}
// [[Rcpp::export]]
arma::vec c_2outmp(const arma::mat M, const arma::vec x, int cores = 4)
{
arma::vec Mx(x.size(), arma::fill::zeros);
omp_set_num_threads(cores);
int r = M.n_rows;
int c = M.n_cols;
#pragma omp parallel for schedule(static)
for (int i = 0; i < r; i++)
{
double val = 0;
for (int j = 0; j < c; j++)
{
val += M(i,j)*x(j);
}
Mx(i) = val;
}
return(Mx);
}
Para analisar o desempenho de cada função utilizaremos o pacote “microbenchmark”, que estima o tempo que cada método levou para ser executado. Multiplicaremos uma matriz de tamanho nxn por um vetor de tamanho n. Primeiramente trabalharemos com o n = 1000 com 10 repetições.
n <- 1000
M <- matrix(runif(n*n), nrow = n)
x <- matrix(runif(n), nrow = n)
base <- microbenchmark(M0 = m_0(M, x),
M1 = m_1(M, x),
M2 = m_2(M, x),
M3 = m_3(M, x),
C0 = c_0(M, x),
C1 = c_1(M, x),
C2 = c_2(M, x),
C2in = c_2inmp(M, x),
C2out = c_2outmp(M, x),times = 10)
Podemos observar os resultados a partir do gráfico de boxplot a seguir.
base %>%
ggplot() +
geom_boxplot(aes(x = reorder(expr, time, median), y = time, fill = expr, alpha = 0.5))
Como podemos ver, o método M2 está muito distante dos outros, dificultando a análise. Iremos portanto eliminá-lo do gráfico para facilitar a visualização
base %>% filter(expr != "M2") %>%
ggplot() +
geom_boxplot(aes(x = reorder(expr, time, median), y = time, fill = expr, alpha = 0.5))
Notamos que as funções implementadas em C tendem a ser mais rápidas do que as implementadas em R, com exceção da função M0 (função do R que utiliza %*% para multiplicação matricial). Investigaremos se esse comportamento permanece ao aumentarmos o n para n = 10000.
n <- 1000
M <- matrix(runif(n*n), nrow = n)
x <- matrix(runif(n), nrow = n)
base2 <- microbenchmark(M0 = m_0(M, x),
M1 = m_1(M, x),
M2 = m_2(M, x),
M3 = m_3(M, x),
C0 = c_0(M, x),
C1 = c_1(M, x),
C2 = c_2(M, x),
C2in = c_2inmp(M, x),
C2out = c_2outmp(M, x),times = 10)
base2 %>% filter(expr != "M2") %>%
ggplot() +
geom_boxplot(aes(x = reorder(expr, time, median), y = time, fill = expr, alpha = 0.5))
Ao aumentar o n para 10.000 o método M0 se consolida como sendo realmente o mais rápido, se destacando mais ainda em relação aos outros visto que a diferença de tempo de execução para os outros métodos aumentou ainda mais. Portanto podemos concluir que para os n testados, as funções definidas aqui tem esse comportamento. Sendo as funções para multiplicação matricial em C geralmente mais rápidas que as funções em R, com exceção do método M0 (que utiliza %*%) que se destaca como o método mais rápido.