The cpp11openmp R package provides an example of parallelizing a C++
routine with OpenMP inside a function exposed to R
with cpp11. cpp11 is a lighter-weight,
header-only alternative to Rcpp, and this example mirrors
rcpp-openmp with the cpp11
toolchain in place of Rcpp.
To install the package, you must first have a compiler on your system that is compatible with R and that supports OpenMP. For help on obtaining a compiler consult either macOS or Windows guides.
With a compiler in hand, one can then install the package from GitHub by:
# install.packages("remotes")
remotes::install_github("coatless-rd-rcpp/cpp11-openmp")
library("cpp11openmp")cpp11 differs from Rcpp in how it connects C++ to R. A C++ function is
marked with the [[cpp11::register]] attribute, and cpp11::cpp_register(), the
counterpart to Rcpp's compileAttributes(), scans the sources and writes two
generated files: src/cpp11.cpp, which registers the native routines, and
R/cpp11.R, which holds the R functions that call them. The example computes
the Euclidean norm of each row of a matrix, dividing the rows across OpenMP
threads.
.
├── DESCRIPTION # Package metadata
├── NAMESPACE # Function and dependency registration
├── R # R functions
│ ├── cpp11.R # Autogenerated R bindings by cpp11
│ ├── cpp11openmp-package.R # Package documentation
│ └── parallel_row_norms.R # Exported wrapper around the compiled routine
├── README.md
├── cpp11openmp.Rproj
├── man # Package documentation
│ ├── cpp11openmp-package.Rd
│ └── parallel_row_norms.Rd
└── src # Compiled code
├── Makevars # Enable OpenMP
├── Makevars.win
├── cpp11.cpp # Autogenerated registration by cpp11
└── parallel_norms.cpp # The OpenMP-parallelized cpp11 routineThe routine lives in src/parallel_norms.cpp. It reads the input matrix and
writes its results into a plain std::vector inside the parallel region, then
converts that vector to an R vector with cpp11::as_sexp() only after the
region has finished. Keeping the conversion outside the threads means the R API
is never touched from inside the parallel region, which is what makes the routine
thread-safe.
#include <cpp11.hpp>
#include <vector>
#include <cmath>
#ifdef _OPENMP
#include <omp.h>
#endif
[[cpp11::register]]
cpp11::doubles cpp_row_norms(cpp11::doubles_matrix<> x) {
int n = x.nrow();
int p = x.ncol();
std::vector<double> out(n);
#pragma omp parallel for schedule(static)
for (int i = 0; i < n; i++) {
double ss = 0.0;
for (int j = 0; j < p; j++) {
double v = x(i, j);
ss += v * v;
}
out[i] = std::sqrt(ss);
}
return cpp11::as_sexp(out);
}The #ifdef _OPENMP guard lets the routine compile without OpenMP, in which
case the loop runs serially.
OpenMP is enabled exactly as it is for an Rcpp package, through
src/Makevars and src/Makevars.win. Both use R's $(SHLIB_OPENMP_CXXFLAGS)
variable, which expands to the correct OpenMP flag for the compiler R was
built with, and supply it to PKG_LIBS so the runtime is linked.
PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS)
PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS)cpp11::cpp_register() generates an internal R function, cpp_row_norms(), in
R/cpp11.R. The package wraps it in an exported, documented function in
R/parallel_row_norms.R that coerces its input to a numeric matrix and calls the
compiled routine.
parallel_row_norms <- function(x) {
storage.mode(x) <- "double"
cpp_row_norms(x)
}cpp11 is header-only and is reached through LinkingTo. Unlike an Rcpp
package, no Imports: Rcpp entry is needed.
LinkingTo:
cpp11
GPL (>= 2)