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
:
::create_project("cpp11omp") usethis
Then, I will add cpp11
as a dependency:
::use_cpp11() usethis
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
NULL
Adding 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();
::doubles y(n);
writable::doubles z(n);
writable#pragma omp parallel for
for (int i = 0; i < n; ++i) {
[i] = x[i] * x[i];
y[i] = omp_get_thread_num();
z}
//create a list containing y and z
::list out;
writable.push_back(y);
out.push_back(z);
outreturn 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
<- function(x) {
squared_unnamed 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();
::doubles y(n);
writable::doubles z(n);
writable#pragma omp parallel for
for (int i = 0; i < n; ++i) {
[i] = x[i] * x[i];
y[i] = omp_get_thread_num();
z}
//create a list containing y and z
::list out;
writable.push_back({"x^2"_nm = y});
out.push_back({"thread"_nm = z});
outreturn 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
<- function(x) {
squared_named 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:
::cpp_register()
cpp11::document()
devtools::install() devtools
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 7 [
Complete code
The complete code is available in this GitHub repository.