Learn to improve performance by rewriting bottlenecks in C++
Introduction to the {Rcpp} package
In this chapter we’ll learn how to rewrite R code in C++ to make it faster using the Rcpp package. The Rcpp package makes it simple to connect C++ to R! With C++ you can fix:
Loops that can’t be easily vectorised because subsequent iterations depend on previous ones.
Recursive functions, or problems which involve calling functions millions of times. The overhead of calling a function in C++ is much lower than in R.
Problems that require advanced data structures and algorithms that R doesn’t provide. Through the standard template library (STL), C++ has efficient implementations of many important data structures, from ordered maps to double-ended queue
Install a C++ compiler:
Rcpp compiling the C++ code:
cppFunction('int add(int x, int y, int z) {
int sum = x + y + z;
return sum;
}')
# add works like a regular R function
add
#> function (x, y, z)
#> .Call(<pointer: 0x00007ff9d7e315f0>, x, y, z)
#> [1] 6
Some things to note:
<-
.int
, double
, bool
and String
IntegerVector
, NumericVector
, LogicalVector
and CharacterVector
List
, Function
, DataFrame
, and more.return
statement to return a value from a function.signR <- function(x) {
if (x > 0) {
1
} else if (x == 0) {
0
} else {
-1
}
}
a <- -0.5
b <- 0.5
c <- 0
signR(c)
#> [1] 0
Translation:
if
syntax is identical! Not everything is different!sumR <- function(x) {
total <- 0
for (i in seq_along(x)) {
total <- total + x[i]
}
total
}
x<- runif(100)
sumR(x)
#> [1] 51.16287
Translation:
Some observations:
.
total += x[i]
is equivalent to total = total + x[i]
.-=
, *=
, and /=
To check for the fastest way we can use:
Note: uses pow()
, not ^
, for exponentiation
Source stand-alone C++ files into R using sourceCpp()
C++ files have extension .cpp
#include <Rcpp.h>
using namespace Rcpp;
And for each function that you want available within R, you need to prefix it with:
// [[Rcpp::export]]
Inside a cpp file you can include R
code using special comments
/*** R
rcode here
*/
This block in Rmarkdown uses {Rcpp}
as a short hand for engine = “Rcpp”.
NOTE: For some reason although the r code above runs, knit
doesn’t include the output. Why?
Contrived example to illustrate how to access a dataframe from c++:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
double mpe(List mod) {
if (!mod.inherits("lm")) stop("Input must be a linear model");
NumericVector resid = as<NumericVector>(mod["residuals"]);
NumericVector fitted = as<NumericVector>(mod["fitted.values"]);
int n = resid.size();
double err = 0;
for(int i = 0; i < n; ++i) {
err += resid[i] / (fitted[i] + resid[i]);
}
return err / n;
}
Other values can be accessed from c++ including
.attr()
. Also .names()
is alias for name attribute.Environment
, DottedPair
, Language
, Symbol
, etc.Scalar NA’s in Cpp : NA_LOGICAL
, NA_INTEGER
, NA_REAL
, NA_STRING
.
Integers (int
) stores R NA’s as the smallest integer. Better to use length 1 IntegerVector
Doubles use IEEE 754 NaN , which behaves a bit differently for logical expressions (but ok for math expressions).
Strings are a class from Rcpp, so they handle missing values fine.
bool
can only hold two values, so be careful. Consider using vectors of length 1 or coercing to int
STL provides powerful data structures and algorithms for C++.
Iterators are used extensively in the STL to abstract away details of underlying data structures.
If you an iterator it
, you can:
*it
++it
==
The real power of iterators comes from using them with STL algorithms.
A good reference is [https://en.cppreference.com/w/cpp/algorithm]
Book provides examples using accumulate
and upper_buond
Another Example:
#include <algorithm>
#include <Rcpp.h>
using namespace Rcpp;
// Explicit iterator version
// [[Rcpp::export]]
NumericVector square_C_it(NumericVector x){
NumericVector out(x.size());
// Each container has its own iterator type
NumericVector::iterator in_it;
NumericVector::iterator out_it;
for(in_it = x.begin(), out_it = out.begin(); in_it != x.end(); ++in_it, ++out_it) {
*out_it = pow(*in_it,2);
}
return out;
}
// Use algorithm 'transform'
// [[Rcpp::export]]
NumericVector square_C(NumericVector x) {
NumericVector out(x.size());
std::transform(x.begin(),x.end(), out.begin(),
[](double v) -> double { return v*v; });
return out;
}
STL provides a large set of data structures. Some of the most important:
std::vector
- like an R
vector, except knows how to grow efficiently
std::unordered_set
- unique set of values. Ordered version std::set
. Unordered is more efficient.
std::map
- Moslty similar to R
lists, provide an association between a key and a value. There is also an unordered version.
A quick example illustrating the map
:
To learn more about the STL data structures see containers at cppreference
Case Study
Real life uses of C++ to replace slow R code.
The Gibbs sampler is a method for estimating parameters expectations. It is a MCMC algorithm that has been adapted to sample from multidimensional target distributions. Gibbs sampling generates a Markov chain of samples, each of which is correlated with nearby samples.
Example blogged by Dirk Eddelbuettel, the R and C++ code is very similar but runs about 20 times faster.
“Darren Wilkinson stresses the rather pragmatic aspects of how fast and/or easy it is to write the code, rather than just the mere runtime.
R code:
Actions to convert R to C++:
(
instead of [
to index into the matrixrgamma
and rnorm
to convert from a vector into a scalar.#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
NumericMatrix gibbs_cpp(int N, int thin) {
NumericMatrix mat(N, 2);
double x = 0, y = 0;
for(int i = 0; i < N; i++) {
for(int j = 0; j < thin; j++) {
x = rgamma(1, 3, 1 / (y * y + 4))[0];
y = rnorm(1, 1 / (x + 1), 1 / sqrt(2 * (x + 1)))[0];
}
mat(i, 0) = x;
mat(i, 1) = y;
}
return(mat);
}
Checking who’s best:
#> # A tibble: 2 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 gibbs_r(100, 10) 1.31ms 3.83ms 283. 103.22KB 18.0
#> 2 gibbs_cpp(100, 10) 150.1µs 416.3µs 2456. 1.61KB 16.9
Rcpp is smoking fast for agent based models in data frames by Gary Weissman, MD, MSHP.
Starts with this code:
R code with a for loop:
Vectorized R code:
C++:
#include <Rcpp.h>
using namespace Rcpp;
double vacc3a(double age, bool female, bool ily){
double p = 0.25 + 0.3 * 1 / (1 - exp(0.04 * age)) + 0.1 * ily;
p = p * (female ? 1.25 : 0.75);
p = std::max(p, 0.0);
p = std::min(p, 1.0);
return p;
}
// [[Rcpp::export]]
NumericVector vacc3(NumericVector age, LogicalVector female,
LogicalVector ily) {
int n = age.size();
NumericVector out(n);
for(int i = 0; i < n; ++i) {
out[i] = vacc3a(age[i], female[i], ily[i]);
}
return out;
}
Sample data:
bench::mark(
vacc1 = vacc1(age, female, ily),
vacc2 = vacc2(age, female, ily),
vacc3 = vacc3(age, female, ily)
)
#> # A tibble: 3 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 vacc1 750.5µs 1.88ms 546. 7.86KB 28.8
#> 2 vacc2 48.4µs 72.1µs 10005. 146.68KB 16.9
#> 3 vacc3 30.8µs 41.5µs 15519. 11.98KB 4.06
Congrats!