元素矩阵乘法:R与Rcpp(如何加速此代码?)

时间:2014-07-24 12:09:01

标签: c++ r eigen rcpp armadillo

我是C++编程的新手(使用Rcpp无缝集成到R),我很感激有关如何加快某些计算的一些建议。

考虑以下示例:

 testmat <- matrix(1:9, nrow=3)
 testvec <- 1:3

 testmat*testvec
   #      [,1] [,2] [,3]
   #[1,]    1    4    7
   #[2,]    4   10   16
   #[3,]    9   18   27

此处,R已回收testvec,因此,松散地说,testvec&#34;成为&#34;用于此乘法的与testmat相同维度的矩阵。然后返回Hadamard产品。我希望使用Rcpp来实现此行为,即我希望矩阵itestmat行的每个元素与i个元素相乘向量testvec。我的基准测试告诉我,我的实现速度非常慢,我很感激建议如何加快速度。这是我的代码:

首先,使用Eigen

#include <RcppEigen.h>
// [[Rcpp::depends(RcppEigen)]]

using namespace Rcpp;
using namespace Eigen;

// [[Rcpp::export]]
NumericMatrix E_matvecprod_elwise(NumericMatrix Xs, NumericVector ys){
  Map<MatrixXd> X(as<Map<MatrixXd> >(Xs));
  Map<VectorXd> y(as<Map<VectorXd> >(ys));

  int k = X.cols();
  int n = X.rows();
  MatrixXd Y(n,k) ;

  // here, I emulate R's recycling. I did not find an easier way of doing this. Any hint appreciated.      
  for(int i = 0; i < k; ++i) {
   Y.col(i) = y;
  }

  MatrixXd out = X.cwiseProduct(Y);
  return wrap(out);
}

此处我的实施使用Armadillo(已调整为遵循Dirk&#39的示例,请参阅下面的答案):

#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]

using namespace Rcpp;
using namespace arma;

// [[Rcpp::export]]
arma::mat A_matvecprod_elwise(const arma::mat & X, const arma::vec & y){
  int k = X.n_cols ;
  arma::mat Y = repmat(y, 1, k) ;  // 
  arma::mat out = X % Y;  
return out;
}

使用R,Eigen或Armadillo对这些解决方案进行基准测试表明,Eigen和Armadillo都比R慢约2倍。有没有办法加快这些计算速度或至少与R一样快?是否有更优雅的方式来设置它?任何建议表示赞赏和欢迎。 (我也鼓励对编程风格进行一致的评论,因为我是Rcpp / C++的新手。)

这里有一些可重复的基准:

 # for comparison, define R function:
 R_matvecprod_elwise <- function(mat, vec) mat*vec

n <- 50000
k <- 50
X <- matrix(rnorm(n*k), nrow=n)
e <- rnorm(n)

benchmark(R_matvecprod_elwise(X, e), A2_matvecprod_elwise(X, e), E_matvecprod_elwise(X,e),
          columns = c("test", "replications", "elapsed", "relative"), order = "relative", replications = 1000)

这会产生

                  test replications elapsed relative
1  R_matvecprod_elwise(X, e)         1000   10.89    1.000
2  A_matvecprod_elwise(X, e)         1000   26.87    2.467
3  E_matvecprod_elwise(X, e)         1000   27.73    2.546

正如您所看到的,我的Rcpp - 解决方案表现相当悲惨。有什么方法可以做得更好吗?

4 个答案:

答案 0 :(得分:9)

如果你想加快计算速度,你必须要小心不要复制。这通常意味着牺牲可读性。这是一个版本,它不会复制并修改矩阵X。

// [[Rcpp::export]]
NumericMatrix Rcpp_matvecprod_elwise(NumericMatrix & X, NumericVector & y){
  unsigned int ncol = X.ncol();
  unsigned int nrow = X.nrow();
  int counter = 0;
  for (unsigned int j=0; j<ncol; j++) {
    for (unsigned int i=0; i<nrow; i++)  {
      X[counter++] *= y[i];
    }
  }
  return X;
}

以下是我在机器上的内容

 > library(microbenchmark)
 > microbenchmark(R=R_matvecprod_elwise(X, e), Arma=A_matvecprod_elwise(X, e),  Rcpp=Rcpp_matvecprod_elwise(X, e))

Unit: milliseconds
 expr       min        lq    median       uq      max neval
    R  8.262845  9.386214 10.542599 11.53498 12.77650   100
 Arma 18.852685 19.872929 22.782958 26.35522 83.93213   100
 Rcpp  6.391219  6.640780  6.940111  7.32773  7.72021   100

> all.equal(R_matvecprod_elwise(X, e), Rcpp_matvecprod_elwise(X, e))
[1] TRUE

答案 1 :(得分:7)

对于初学者,我会将Armadillo版本(界面)写为

#include <RcppArmadillo.h>
// [[Rcpp::depends(RcppArmadillo)]]

using namespace Rcpp;
using namespace arma;

// [[Rcpp::export]]
arama::mat A_matvecprod_elwise(const arma::mat & X, const arma::vec & y){
  int k = X.n_cols ;
  arma::mat Y = repmat(y, 1, k) ;  // 
  arma::mat out = X % Y;  
  return out;
}

因为您正在进行额外的转换(尽管胶水代码添加了wrap())。 const &是名义上的(正如您在上一个问题中所了解到的,SEXP是一个轻量级的指针对象,但更好的风格)。

你没有显示你的基准测试结果所以我无法评论矩阵大小等的影响pp。我怀疑你可能会在rcpp-devel上找到比这里更好的答案。你的选择。

编辑:如果您真的想要便宜又快速的东西,我会这样做:

// [[Rcpp::export]]
mat cheapHadamard(mat X, vec y) {
    // should row dim of X versus length of Y here
    for (unsigned int i=0; i<y.n_elem; i++) X.row(i) *= y(i);
    return X;
}

不分配新内存,因此速度更快,可能与R竞争。

测试输出:

R> cheapHadamard(testmat, testvec)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    4   10   16
[3,]    9   18   27
R> 

答案 2 :(得分:3)

我很抱歉给出了一个C ++问题的基本C答案,但正如所建议的那样,解决方案通常在于有效的BLAS实现。不幸的是,BLAS本身缺少Hadamard乘法,所以你必须实现自己的。

这是一个纯粹的Rcpp实现,基本上调用C代码。如果你想使它成为正确的C ++,那么worker函数可以被模板化,但对于大多数使用R的应用程序而言并不是一个问题。请注意,这也是“就地”操作,这意味着它修改X而不复制它。

// it may be necessary on your system to uncomment one of the following
//#define restrict __restrict__ // gcc/clang
//#define restrict __restrict   // MS Visual Studio
//#define restrict              // remove it completely

#include <Rcpp.h>
using namespace Rcpp;

#include <cstdlib>
using std::size_t;

void hadamardMultiplyMatrixByVectorInPlace(double* restrict x,
                                           size_t numRows, size_t numCols,
                                           const double* restrict y)
{
  if (numRows == 0 || numCols == 0) return;

  for (size_t col = 0; col < numCols; ++col) {
    double* restrict x_col = x + col * numRows;

    for (size_t row = 0; row < numRows; ++row) {
      x_col[row] *= y[row];
    }
  }
}

// [[Rcpp::export]]
NumericMatrix C_matvecprod_elwise_inplace(NumericMatrix& X,
                                          const NumericVector& y)
{
  // do some dimension checking here

  hadamardMultiplyMatrixByVectorInPlace(X.begin(), X.nrow(), X.ncol(),
                                        y.begin());

  return X;
}

这是一个先制作副本的版本。我不太了解Rcpp本身做到这一点,并没有产生实质性的打击。在堆栈上创建并返回NumericMatrix(numRows, numCols)会导致代码运行速度降低约30%。

#include <Rcpp.h>
using namespace Rcpp;

#include <cstdlib>
using std::size_t;

#include <R.h>
#include <Rdefines.h>

void hadamardMultiplyMatrixByVector(const double* restrict x,
                                    size_t numRows, size_t numCols,
                                    const double* restrict y,
                                    double* restrict z)
{
  if (numRows == 0 || numCols == 0) return;

  for (size_t col = 0; col < numCols; ++col) {
    const double* restrict x_col = x + col * numRows;
    double* restrict z_col = z + col * numRows;

    for (size_t row = 0; row < numRows; ++row) {
      z_col[row] = x_col[row] * y[row];
    }
  }
}

// [[Rcpp::export]]
SEXP C_matvecprod_elwise(const NumericMatrix& X, const NumericVector& y)
{
  size_t numRows = X.nrow();
  size_t numCols = X.ncol();

  // do some dimension checking here

  SEXP Z = PROTECT(Rf_allocVector(REALSXP, (int) (numRows * numCols)));
  SEXP dimsExpr = PROTECT(Rf_allocVector(INTSXP, 2));
  int* dims = INTEGER(dimsExpr);
  dims[0] = (int) numRows;
  dims[1] = (int) numCols;
  Rf_setAttrib(Z, R_DimSymbol, dimsExpr);

  hadamardMultiplyMatrixByVector(X.begin(), X.nrow(), X.ncol(), y.begin(), REAL(Z));

  UNPROTECT(2);

  return Z;
}

如果您对restrict的使用感到好奇,那就意味着您作为程序员与编译器签订了一份不同内存不重叠的合同,允许编译器进行某些优化。 restrict关键字是C ++ 11(和C99)的一部分,但许多编译器为早期标准添加了C ++扩展。

一些用于基准测试的R代码:

require(rbenchmark)

n <- 50000
k <- 50
X <- matrix(rnorm(n*k), nrow=n)
e <- rnorm(n)

R_matvecprod_elwise <- function(mat, vec) mat*vec

all.equal(R_matvecprod_elwise(X, e), C_matvecprod_elwise(X, e))
X_dup <- X + 0
all.equal(R_matvecprod_elwise(X, e), C_matvecprod_elwise_inplace(X_dup, e))

benchmark(R_matvecprod_elwise(X, e),
          C_matvecprod_elwise(X, e),
          C_matvecprod_elwise_inplace(X, e),
          columns = c("test", "replications", "elapsed", "relative"),
          order = "relative", replications = 1000)

结果:

                               test replications elapsed relative
3 C_matvecprod_elwise_inplace(X, e)         1000   3.317    1.000
2         C_matvecprod_elwise(X, e)         1000   7.174    2.163
1         R_matvecprod_elwise(X, e)         1000  10.670    3.217

最后,就地版本实际上可能更快,因为重复乘法到同一矩阵可能会导致一些溢出混乱。

编辑:

删除了展开的循环,因为它没有提供任何好处,而是分散注意力。

答案 3 :(得分:2)

我想以Sameer的答案为基础,但我没有足够的声誉来评论。

我个人在Eigen中的表现更好(约50%):

return (y.asDiagonal() * X);

尽管有外观,但这并不会为n x n创建y临时。