为什么我示例中的 Rcpp 实现比 R 函数慢得多?
Why is the Rcpp implementation in my example much slower than the R function?
我在 C++ 和 R 方面有一些经验,但我是 Rcpp 的新手。最近我在之前的一些项目中使用 Rcpp 取得了巨大的成功,所以决定将它应用到一个新项目中。我很惊讶我的 Rcpp 代码可能比相应的 R 函数慢得多。我试图简化我的 R 函数以找出原因,但找不到任何线索。非常欢迎您的帮助和评论!
比较 R 和 Rcpp 实现的主要 R 函数:
main <- function(){
n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
print(microbenchmark(c1 <- test(Delta, delta, rx), times = 500))
print(microbenchmark(c2 <- rcpp_test(Delta, delta, rx), times = 500))
identical(c1, c2)
list(c1 = c1, c2 = c2)
}
R 实现:
test <- function(Delta, delta, rx){
const <- list()
for(i in 1:ncol(delta)){
const[[i]] <- rx * (Delta / (1 + delta[, i]))
}
const
}
Rcpp 实现:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
List rcpp_test(NumericVector Delta,
NumericMatrix delta,
NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
List c;
NumericMatrix c1;
for(int i = 0; i < delta.ncol(); ++i){
c1 = NumericMatrix(n, m);
for(int k = 0; k < n; ++k){
double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < c1.ncol(); ++j){
c1(k, j) = rx(k, j) * tmp;
}
}
c.push_back(c1);
}
return c;
}
我知道使用 Rcpp 并不能保证提高效率,但是考虑到我在这里展示的简单示例,我不明白为什么 Rcpp 代码运行得这么慢。
Unit: milliseconds
expr min lq mean median uq max neval
c1 <- test(Delta, delta, rx) 13.16935 14.19951 44.08641 30.43126 73.78581 115.9645 500
Unit: milliseconds
expr min lq mean median uq max neval
c2 <- rcpp_test(Delta, delta, rx) 143.1917 158.7481 171.6116 163.413 173.7677 247.5495 500
理想情况下 rx
是我项目中的矩阵列表。 for 循环中的变量 i
将用于选择一个元素进行计算。一开始我怀疑将 List
传递给 Rcpp 可能会有很高的开销,所以在这个例子中,我假设 rx
是一个固定矩阵,用于所有 i
。看来这不是缓慢的原因。
您的 R 代码似乎或多或少是最优的,即所有实际工作都是在编译代码中完成的。对于 C++ 代码,我发现的主要问题是在紧密循环中调用 c1.ncol()
。如果我用 m
替换它,C++ 解决方案几乎和 R 一样快。如果我将 RcppArmadillo 添加到组合中,我会得到一个非常紧凑的语法,但并不比纯 Rcpp 代码快。对我来说,这表明要击败编写良好的 R 代码真的很难:
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
using namespace Rcpp;
// [[Rcpp::export]]
List arma_test(const arma::vec& Delta,
const arma::mat& delta,
const arma::mat& rx) {
int l = delta.n_cols;
List c(l);
for (int i = 0; i < l; ++i) {
c(i) = rx.each_col() % (Delta / (1 + delta.col(i)));
}
return c;
}
// [[Rcpp::export]]
List rcpp_test(NumericVector Delta,
NumericMatrix delta,
NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
List c(delta.ncol());
NumericMatrix c1;
for(int i = 0; i < delta.ncol(); ++i){
c1 = NumericMatrix(n, m);
for(int k = 0; k < n; ++k){
double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < m; ++j){
c1(k, j) = rx(k, j) * tmp;
}
}
c(i) = c1;
}
return c;
}
/*** R
test <- function(Delta, delta, rx){
const <- list()
for(i in 1:ncol(delta)){
const[[i]] <- rx * (Delta / (1 + delta[, i]))
}
const
}
n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
bench::mark(test(Delta, delta, rx),
arma_test(Delta, delta, rx),
rcpp_test(Delta, delta, rx))
*/
输出:
# A tibble: 3 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr
<chr> <bch:t> <bch:t> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int>
1 test(Delt… 84.3ms 85.2ms 84.9ms 86.6ms 11.7 44.9MB 2 4
2 arma_test… 106.5ms 107.7ms 107.7ms 108.9ms 9.28 38.1MB 3 2
3 rcpp_test… 101.9ms 103.2ms 102.2ms 106.6ms 9.69 38.1MB 1 4
# … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
# time <list>, gc <list>
我还明确地将输出列表初始化为所需的大小,避免了 push_back
,但这并没有太大的区别。但是,对于来自 Rcpp 的类似向量的数据结构,您绝对应该避免使用 push_back
,因为每次扩展向量时都会创建一个副本。
我想补充@RalfStubner 的出色回答。
您会注意到我们在第一个 for 循环中进行了很多分配(即 c1 = NumerMatrix(n, m)
)。这可能很昂贵,因为除了分配内存之外,我们还将每个元素初始化为 0。我们可以将其更改为以下内容以提高效率:
NumericMatrix c1 = no_init_matrix(n, m)
我还尽可能地添加了关键字 const
。这样做是否允许编译器优化某些代码片段是值得商榷的,但为了代码清晰,我仍然在可以的地方添加它(即 "I don't want this variable to change")。这样,我们有:
// [[Rcpp::export]]
List rcpp_test_modified(const NumericVector Delta,
const NumericMatrix delta,
const NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
int dCol = delta.ncol();
List c(dCol);
for(int i = 0; i < dCol; ++i) {
NumericMatrix c1 = no_init_matrix(n, m);
for(int k = 0; k < n; ++k) {
const double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < m; ++j) {
c1(k, j) = rx(k, j) * tmp;
}
}
c[i] = c1;
}
return c;
}
这里有一些基准(Armadillo
解决方案遗漏了):
bench::mark(test(Delta, delta, rx),
rcpp_test_modified(Delta, delta, rx),
rcpp_test(Delta, delta, rx))
# A tibble: 3 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr total_time result memory time
<chr> <bch:t> <bch:> <bch:t> <bch:> <dbl> <bch:byt> <dbl> <int> <bch:tm> <list> <list> <lis>
1 test(Delt… 12.27ms 17.2ms 14.56ms 29.5ms 58.1 41.1MB 13 8 138ms <list… <Rpro… <bch…
2 rcpp_test… 7.55ms 11.4ms 8.46ms 26ms 87.8 38.1MB 16 21 239ms <list… <Rpro… <bch…
3 rcpp_test… 10.36ms 15.8ms 13.64ms 28.9ms 63.4 38.1MB 10 17 268ms <list… <Rpro… <bch…
# … with 1 more variable: gc <list>
我们看到 50%
版本的 Rcpp
改进。
我在 C++ 和 R 方面有一些经验,但我是 Rcpp 的新手。最近我在之前的一些项目中使用 Rcpp 取得了巨大的成功,所以决定将它应用到一个新项目中。我很惊讶我的 Rcpp 代码可能比相应的 R 函数慢得多。我试图简化我的 R 函数以找出原因,但找不到任何线索。非常欢迎您的帮助和评论!
比较 R 和 Rcpp 实现的主要 R 函数:
main <- function(){
n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
print(microbenchmark(c1 <- test(Delta, delta, rx), times = 500))
print(microbenchmark(c2 <- rcpp_test(Delta, delta, rx), times = 500))
identical(c1, c2)
list(c1 = c1, c2 = c2)
}
R 实现:
test <- function(Delta, delta, rx){
const <- list()
for(i in 1:ncol(delta)){
const[[i]] <- rx * (Delta / (1 + delta[, i]))
}
const
}
Rcpp 实现:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
List rcpp_test(NumericVector Delta,
NumericMatrix delta,
NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
List c;
NumericMatrix c1;
for(int i = 0; i < delta.ncol(); ++i){
c1 = NumericMatrix(n, m);
for(int k = 0; k < n; ++k){
double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < c1.ncol(); ++j){
c1(k, j) = rx(k, j) * tmp;
}
}
c.push_back(c1);
}
return c;
}
我知道使用 Rcpp 并不能保证提高效率,但是考虑到我在这里展示的简单示例,我不明白为什么 Rcpp 代码运行得这么慢。
Unit: milliseconds
expr min lq mean median uq max neval
c1 <- test(Delta, delta, rx) 13.16935 14.19951 44.08641 30.43126 73.78581 115.9645 500
Unit: milliseconds
expr min lq mean median uq max neval
c2 <- rcpp_test(Delta, delta, rx) 143.1917 158.7481 171.6116 163.413 173.7677 247.5495 500
理想情况下 rx
是我项目中的矩阵列表。 for 循环中的变量 i
将用于选择一个元素进行计算。一开始我怀疑将 List
传递给 Rcpp 可能会有很高的开销,所以在这个例子中,我假设 rx
是一个固定矩阵,用于所有 i
。看来这不是缓慢的原因。
您的 R 代码似乎或多或少是最优的,即所有实际工作都是在编译代码中完成的。对于 C++ 代码,我发现的主要问题是在紧密循环中调用 c1.ncol()
。如果我用 m
替换它,C++ 解决方案几乎和 R 一样快。如果我将 RcppArmadillo 添加到组合中,我会得到一个非常紧凑的语法,但并不比纯 Rcpp 代码快。对我来说,这表明要击败编写良好的 R 代码真的很难:
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
using namespace Rcpp;
// [[Rcpp::export]]
List arma_test(const arma::vec& Delta,
const arma::mat& delta,
const arma::mat& rx) {
int l = delta.n_cols;
List c(l);
for (int i = 0; i < l; ++i) {
c(i) = rx.each_col() % (Delta / (1 + delta.col(i)));
}
return c;
}
// [[Rcpp::export]]
List rcpp_test(NumericVector Delta,
NumericMatrix delta,
NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
List c(delta.ncol());
NumericMatrix c1;
for(int i = 0; i < delta.ncol(); ++i){
c1 = NumericMatrix(n, m);
for(int k = 0; k < n; ++k){
double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < m; ++j){
c1(k, j) = rx(k, j) * tmp;
}
}
c(i) = c1;
}
return c;
}
/*** R
test <- function(Delta, delta, rx){
const <- list()
for(i in 1:ncol(delta)){
const[[i]] <- rx * (Delta / (1 + delta[, i]))
}
const
}
n <- 50000
Delta <- exp(rnorm(n))
delta <- exp(matrix(rnorm(n * 5), nrow = n))
rx <- matrix(rnorm(n * 20), nrow = n)
bench::mark(test(Delta, delta, rx),
arma_test(Delta, delta, rx),
rcpp_test(Delta, delta, rx))
*/
输出:
# A tibble: 3 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr
<chr> <bch:t> <bch:t> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int>
1 test(Delt… 84.3ms 85.2ms 84.9ms 86.6ms 11.7 44.9MB 2 4
2 arma_test… 106.5ms 107.7ms 107.7ms 108.9ms 9.28 38.1MB 3 2
3 rcpp_test… 101.9ms 103.2ms 102.2ms 106.6ms 9.69 38.1MB 1 4
# … with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
# time <list>, gc <list>
我还明确地将输出列表初始化为所需的大小,避免了 push_back
,但这并没有太大的区别。但是,对于来自 Rcpp 的类似向量的数据结构,您绝对应该避免使用 push_back
,因为每次扩展向量时都会创建一个副本。
我想补充@RalfStubner 的出色回答。
您会注意到我们在第一个 for 循环中进行了很多分配(即 c1 = NumerMatrix(n, m)
)。这可能很昂贵,因为除了分配内存之外,我们还将每个元素初始化为 0。我们可以将其更改为以下内容以提高效率:
NumericMatrix c1 = no_init_matrix(n, m)
我还尽可能地添加了关键字 const
。这样做是否允许编译器优化某些代码片段是值得商榷的,但为了代码清晰,我仍然在可以的地方添加它(即 "I don't want this variable to change")。这样,我们有:
// [[Rcpp::export]]
List rcpp_test_modified(const NumericVector Delta,
const NumericMatrix delta,
const NumericMatrix rx) {
int n = Delta.length();
int m = rx.ncol();
int dCol = delta.ncol();
List c(dCol);
for(int i = 0; i < dCol; ++i) {
NumericMatrix c1 = no_init_matrix(n, m);
for(int k = 0; k < n; ++k) {
const double tmp = Delta[k] / (1 + delta(k, i));
for(int j = 0; j < m; ++j) {
c1(k, j) = rx(k, j) * tmp;
}
}
c[i] = c1;
}
return c;
}
这里有一些基准(Armadillo
解决方案遗漏了):
bench::mark(test(Delta, delta, rx),
rcpp_test_modified(Delta, delta, rx),
rcpp_test(Delta, delta, rx))
# A tibble: 3 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr total_time result memory time
<chr> <bch:t> <bch:> <bch:t> <bch:> <dbl> <bch:byt> <dbl> <int> <bch:tm> <list> <list> <lis>
1 test(Delt… 12.27ms 17.2ms 14.56ms 29.5ms 58.1 41.1MB 13 8 138ms <list… <Rpro… <bch…
2 rcpp_test… 7.55ms 11.4ms 8.46ms 26ms 87.8 38.1MB 16 21 239ms <list… <Rpro… <bch…
3 rcpp_test… 10.36ms 15.8ms 13.64ms 28.9ms 63.4 38.1MB 10 17 268ms <list… <Rpro… <bch…
# … with 1 more variable: gc <list>
我们看到 50%
版本的 Rcpp
改进。