Debunking the idea that cpp11 does not offer OpenMP support
R and Shiny Training: If you find this blog to be interesting, please note that I offer personalized and group-based training sessions that may be reserved through Buy me a Coffee. Additionally, I provide training services in the Spanish language and am available to discuss means by which I may contribute to your Shiny project.
Motivation
One common phrase that I find when I need to Google how to do something with cpp11 is “Don’t use cpp11 because it does not offer OpenMP support.”
This is a myth. cpp11 does offer OpenMP support. In this blog post, I will show you how to use OpenMP with cpp11, but here I assume your C++ compiler already supports OpenMP.
I tested this on Windows, where you need to install Rtools, and Linux Mint (Ubuntu based) where I didn’t need anything special because the gcc compiler comes with the operating system and just works. If you are using macOS, you need to install libomp via Homebrew in order to extend the clang compiler, and this is explained here.
Creating a package
First, we need to create a package. I will use usethis to create a package called cpp11omp:
usethis::create_project("cpp11omp")Then, I will add cpp11 as a dependency:
usethis::use_cpp11()As the cpp11 message indicates, I created a file called R/cpp11omp-package.R with the following contents:
## usethis namespace: start
#' @useDynLib cpp11omp, .registration = TRUE
## usethis namespace: end
NULLAdding functions
Cpp11 unnamed list
I added a function called squared_unnamed_ in src/code.cpp that will square each element in a vector of doubles, so the file content corresponds to the following:
#include <cpp11.hpp>
#include <omp.h>
using namespace cpp11;
[[cpp11::register]] list squared_unnamed_(doubles x) {
// create vectors y = x^2 and z = thread number
int n = x.size();
writable::doubles y(n);
writable::doubles z(n);
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
y[i] = x[i] * x[i];
z[i] = omp_get_thread_num();
}
//create a list containing y and z
writable::list out;
out.push_back(y);
out.push_back(z);
return out;
}The previous function returns an unnamed list with two elements: the squared vector and the thread number. The function is registered with [[cpp11::register]] so that it can be called from R.
If I try to run square_unnamed_(1:10), it will return an error because I am passing a vector of integers instead of doubles. C++ is strict with types, so I need to create a wrapper function that will convert the integers to doubles, and it will go inside R/cpp11omp-package.R:
#' Unnamed list with squared numbers and the threads used
#' @param x A vector of doubles
#' @export
squared_unnamed <- function(x) {
squared_unnamed_(as.double(x))
}The previous function is exported with @export so that it can be called by the end user. The function squared_unnamed_ is an internal function. This approach also has the advantage that I can document the function in a flexible way.
Cpp11 named list
I added a function called squared_named_ in src/code.cpp that does the same but returns a named list. The additional content corresponds to the following:
[[cpp11::register]] list squared_named_(doubles x) {
// create vectors y = x^2 and z = thread number
int n = x.size();
writable::doubles y(n);
writable::doubles z(n);
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
y[i] = x[i] * x[i];
z[i] = omp_get_thread_num();
}
//create a list containing y and z
writable::list out;
out.push_back({"x^2"_nm = y});
out.push_back({"thread"_nm = z});
return out;
}As in the previous part, I added a wrapper and documentation:
#' Named list with squared numbers and the threads used
#' @param x A vector of doubles
#' @export
squared_named <- function(x) {
squared_named_(as.double(x))
}Makevars
In order to make the #pragma instruction work, I need to add the following to src/Makevars:
PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS)
PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS)
CXX_STD = CXX11
If I don’t do this, the pragma instruction will be ignored and the functions will run in a single thread.
Building and testing
I used devtools to build and test the package:
cpp11::cpp_register()
devtools::document()
devtools::install()Then, I tested the package from a new R session:
> library(cpp11omp)
> squared_unnamed(1:10)
[[1]]
[1] 1 4 9 16 25 36 49 64 81 100
[[2]]
[1] 0 0 1 1 2 3 4 5 6 7
> squared_named(1:10)
$`x^2`
[1] 1 4 9 16 25 36 49 64 81 100
$thread
[1] 0 0 1 1 2 3 4 5 6 7Complete code
The complete code is available in this GitHub repository.