C++ 元素矩阵乘法:R与Rcpp(如何加快代码速度?)

C++ 元素矩阵乘法:R与Rcpp(如何加快代码速度?),c++,r,eigen,rcpp,armadillo,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 下面是我使用犰狳(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
下面是我使用犰狳(Armadillo)的实现(根据Dirk的示例进行调整,请参见下面的答案):

这就产生了

                  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
-解决方案的性能相当糟糕。有什么方法可以做得更好吗?

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

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

测试输出:

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

如果你想加快你的计算速度,你必须小心不要复制。这通常意味着牺牲可读性。这是一个版本,它不复制并修改矩阵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;
}

我对给出C++问题的实质性答案表示歉意,但正如已经提出的,解决方案一般在于有效的BLAS实现。不幸的是,BLAS本身缺少一个Hadamard乘法,因此您必须实现自己的乘法

这里是一个纯Rcpp实现,基本上调用C代码。如果您想使它成为适当的C++,可以用Word函数模板化,但对于大多数应用程序来说,使用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;
}
如果您对的使用感到好奇,这意味着您作为程序员与编译器签订了一项契约,不同的内存位不会重叠,从而允许编译器进行某些优化。“代码>限制>关键字是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)
最后,就地版本实际上可能更快,因为重复乘法到同一矩阵中可能会造成一些溢出混乱

编辑:


取消了循环展开,因为它没有提供任何好处,而且会分散注意力。

再次感谢德克的支持。我试着使用你的
cheapHadamard
-函数,但结果却是最慢的,这让我感到惊讶。你知道为什么它会这么慢吗?也许跨矩阵的行访问速度很慢。不知道现在你可以看到这个概要——或者看看(毕竟是开源的)R代码,它打败了我们所有人。它可能只是调用一个更快的BLAS例程……您可以执行
X=X.cwiseProduct(y.replicate(1,X.cols())在特征值中,然后
返回Xs但使用1e3*1e3矩阵并没有改善我系统上的基准测试,而不是你的本征函数。在犰狳中,尝试.each_row()函数:沿着以下几行:X.each_row()%=l靠近你的代码,使用它可能更正确:
X.each_col()%=y
谢谢@mtall!您建议的更改极大地加快了
arma
-功能的运行速度,因此现在它比
R
快,尽管Rcpp版本在这里仍然优于
arma
。无论如何,谢谢你的评论。非常感谢您的帮助。@coffeinjunky-这是代码可读性、紧凑性和速度之间的折衷。虽然您总是可以编写针对手头特定任务进行优化的低级代码,但它几乎总是以更难阅读和维护为代价(因此您更有可能创建bug)。通常,只使用库提供的工具更安全、更容易,例如.each_col()方法。这看起来非常好。这是我第一次运行代码来击败
R
!如果您不介意我问,为什么将所有整数声明为
无符号
会有帮助?这排除了负值,但为什么速度会提高?另外,我不确定我是否理解这行
X[counter++]*=y[I]
counter++
在这里做什么,以及
j
发生了什么?我非常感激你能给我一个简短的评论……我不认为没有签名会有什么影响。如果基准点因此发生了重大变化,我会感到惊讶。速度的提高是因为我们不复制向量或矩阵。计数器变量是为了避免计算出(i,j)索引在矩阵中的位置,方法是利用R如何在矩阵中布局元素。是的,是内存布局问题使它更快。这个解决方案是唯一一个通过矩阵中的列进行操作的解决方案,只需查找正确的标量即可。其他一切都必须组装行向量,而这并没有那么便宜。而
counter++
是一个“简单”的好技巧,可以将矩阵作为一个长向量,一列接一列地遍历,从而提高了速度。非常感谢您的回答!这看起来非常令人印象深刻,虽然我不得不承认我还不了解很多事情,但我希望有一天会。我会尽快让它运行的!当我将其与其他解决方案一起进行基准测试时,它的速度并没有那么快。我使用了包装器调用C函数的最后一种方法。您是否在有无显式循环展开的情况下对代码计时?许多编译器将自己执行展开,显式展开使代码更难阅读。除此之外,您可能会进行调查。就个人而言,我对这种面向用户的高级代码的可读性较差的解决方案不太感兴趣(尽管我们确实有一些隐藏在side Rcpp Sugar中)。我制作了一个包含4个版本的算法的包:常规、展开、就地和就地+展开。一开始,就地测试总是比就地+展开测试慢,展开的速度几乎和R一样慢。我花了一段时间才弄明白,但正是颠倒了就地测试的顺序。实现一个纯C基准测试,在该基准测试中,只需对函数调用进行计时就可以始终生成IP(2.6s)// [[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> cheapHadamard(testmat, testvec)
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    4   10   16
[3,]    9   18   27
R> 
// [[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
// 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;
}
#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;
}
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