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