Skip to contents

Using OpenMP with cpp11

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.

Usage

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")

Implementation Details

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 routine

The Parallel C++ Routine

The 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.

Enabling OpenMP with Makevars

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)

From the Compiled Routine to R

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)
}

DESCRIPTION

cpp11 is header-only and is reached through LinkingTo. Unlike an Rcpp package, no Imports: Rcpp entry is needed.

LinkingTo:
    cpp11

License

GPL (>= 2)