问题
I have defined the following function:
pij = function(vec){
out = vec %*% t(vec)
diag(out) = NA
out = sum(out, na.rm = T)
return(out)
}
where vec
is a vector, for instance vec = rnorm(10^4,0,1)
.
I would like to know how this function can be written in C++ using the Rcpp package.
回答1:
I would suggest thinking about the math behind the problem first. For a vector v
you are trying to calculate
sum_{i=1}^{N-1} sum_{j=i+1}^{N} 2 * v_i * v_j
You can do that by creating the matrix v_i * v_j
first, but that can be expensive if v
is large. So it is easier to implement the double sum directly in C++:
#include <Rcpp.h>
// [[Rcpp::export]]
double pij_cpp(Rcpp::NumericVector vec) {
double out{0.0};
int N = vec.size();
for (int i = 0; i < N; ++i) {
for (int j = i + 1; j < N; ++j) {
out += 2 * vec[i] * vec[j];
}
}
return out;
}
However, the formula above can actually be rearranged:
2 * sum_{i=1}^{N-1} v_i * sum_{j=i+1}^{N} v_j
which allows us to get rid of the double loop by starting at the high end and going to the low end:
#include <Rcpp.h>
// [[Rcpp::export]]
double pij_opt(Rcpp::NumericVector vec) {
double out{0.0};
double sum{0.0};
int N = vec.size();
for (int i = N -1; i > 0; --i) {
sum += vec[i];
out += sum * vec[i-1];
}
return 2 * out;
}
We can compare these versions with your R code and an Armadillo based version for a vector of length 10^4
:
> bench::mark(pij(vec), pij_cpp(vec), pij_opt(vec), pij_arma(vec))
# A tibble: 4 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr total_time result
<chr> <bch:t> <bch:t> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int> <bch:tm> <list>
1 pij(vec) 716.4ms 716.4ms 716.4ms 716.4ms 1.40 1.49GB 1 1 716ms <dbl …
2 pij_cpp(v… 59.9ms 61.4ms 61.5ms 62.3ms 16.3 2.49KB 0 9 552ms <dbl …
3 pij_opt(v… 14.2µs 15.6µs 14.9µs 864.5µs 64072. 2.49KB 0 10000 156ms <dbl …
4 pij_arma(… 834.5ms 834.5ms 834.5ms 834.5ms 1.20 2.49KB 0 1 834ms <dbl …
# ... with 3 more variables: memory <list>, time <list>, gc <list>
R and Armadillo are about on par (and probably limited by the memory allocation). The first C++ version is faster by a factor of 10, the second by a factor of 50000!
Full code:
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
// [[Rcpp::export]]
double pij_arma(arma::vec vec) {
arma::mat out = vec * vec.t();
out.diag().zeros();
return arma::accu(out);
}
// [[Rcpp::export]]
double pij_cpp(Rcpp::NumericVector vec) {
double out{0.0};
int N = vec.size();
for (int i = 0; i < N; ++i) {
for (int j = i + 1; j < N; ++j) {
out += 2 * vec[i] * vec[j];
}
}
return out;
}
// [[Rcpp::export]]
double pij_opt(Rcpp::NumericVector vec) {
double out{0.0};
double sum{0.0};
int N = vec.size();
for (int i = N -1; i > 0; --i) {
sum += vec[i];
out += sum * vec[i-1];
}
return 2 * out;
}
/*** R
pij = function(vec){
out = vec %*% t(vec)
diag(out) = NA
out = sum(out, na.rm = T)
return(out)
}
set.seed(42)
vec = rnorm(10^4,0,1)
pij(vec)
bench::mark(pij(vec), pij_cpp(vec), pij_opt(vec), pij_arma(vec))
*/
For completeness: This is really a question of algorithm, so even a for
loop in R is faster than pij_cpp
:
pij_opt_r <- function(vec) {
out <- 0
sum <- 0
N <- length(vec)
for (i in seq.int(from = N, to = 2, by = -1)) {
sum <- sum + vec[i]
out <- out + sum * vec[i-1]
}
2 * out
}
Using vectorized functions in R is even faster, but still not as fast as pij_opt
:
pij_opt_r2 <- function(vec) {
N <- length(vec)
vec <- rev(vec)
sums <- cumsum(vec)
2 * sum(vec[2:N] * sums[1:N-1])
}
Full benchmark:
> bench::mark(pij(vec), pij_cpp(vec), pij_opt(vec), pij_opt_r(vec), pij_opt_r2(vec), pij_arma(vec))
# A tibble: 6 x 14
expression min mean median max `itr/sec` mem_alloc n_gc n_itr total_time result
<chr> <bch:t> <bch:tm> <bch:tm> <bch:t> <dbl> <bch:byt> <dbl> <int> <bch:tm> <list>
1 pij(vec) 733.6ms 733.6ms 733.6ms 733.6ms 1.36 1.49GB 1 1 734ms <dbl …
2 pij_cpp(v… 60ms 61.41ms 60.84ms 64.2ms 16.3 2.49KB 0 9 553ms <dbl …
3 pij_opt(v… 14.2µs 15.83µs 15.35µs 750.1µs 63164. 2.49KB 0 10000 158ms <dbl …
4 pij_opt_r… 981.1µs 1.04ms 1.02ms 1.5ms 960. 119.2KB 0 480 500ms <dbl …
5 pij_opt_r… 157µs 272.95µs 241.57µs 66.3ms 3664. 547.28KB 1 1832 500ms <dbl …
6 pij_arma(… 878.4ms 878.38ms 878.38ms 878.4ms 1.14 2.49KB 0 1 878ms <dbl …
# ... with 3 more variables: memory <list>, time <list>, gc <list>
回答2:
Here is a better, more direct version where C++ ends up winning a little:
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
// [[Rcpp::export]]
double pij_cpp(const arma::vec & v) {
arma::mat m = v * v.t();
m.diag().zeros();
double s = arma::as_scalar(arma::accu(m));
return(s);
}
/*** R
library(rbenchmark)
set.seed(123)
pij <- function(vec){
out <- vec %*% t(vec)
diag(out) <- NA
out <- sum(out, na.rm = T)
}
x <- rnorm(1000)
## make sure they are the same
all.equal(pij(x), pij_cpp(x))
## benchmark
benchmark(R=pij(x), Cpp=pij_cpp(x))
*/
On my machine, C++ is ahead:
R> sourceCpp("~/git/so-r/53105055/answer.cpp")
R> library(rbenchmark)
R> set.seed(123)
R> pij <- function(vec){
+ out <- vec %*% t(vec)
+ diag(out) <- NA
+ out <- sum(out, na.rm = T)
+ }
R> x <- rnorm(1000)
R> ## make sure they are the same
R> all.equal(pij(x), pij_cpp(x))
[1] TRUE
R> ## benchmark
R> benchmark(R=pij(x), Cpp=pij_cpp(x))
test replications elapsed relative user.self sys.self user.child sys.child
2 Cpp 100 0.127 1.000 0.283 0.356 0 0
1 R 100 0.583 4.591 2.607 4.011 0 0
R>
The bigger takeaway is ... that you looked at the wrong problem. Your R function is already highly vectorized and calls out to mostly compiled code so there was not that much to gain.
回答3:
This is the solution that I have found:
library(Rcpp)
library(inline)
rcpp_inc = "using namespace Rcpp;
using namespace arma;"
src = "
vec vec1 = as<vec>(vecin);
mat out = vec1*trans(vec1);
out.diag().zeros();
return(wrap(accu(out)));
"
pij_rcpp = cxxfunction(signature(vecin="numeric"), src, plugin='RcppArmadillo', rcpp_inc)
However, it is slower than the function written in R. For instance, if I run this example,
set.seed(1)
x = runif(1e4)
system.time({pij_r(x)})
system.time({pij_rcpp(x)})
I get that the elapsed period is 1.101 for pij_r
and 1.323 for pij_rcpp
.
来源:https://stackoverflow.com/questions/53105055/how-to-convert-a-r-function-into-a-c-function-using-rcpp