Debunking the idea that cpp11 does not offer OpenMP support

[This article was first published on pacha.dev/blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

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
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();
    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 7

Complete code

The complete code is available in this GitHub repository.

To leave a comment for the author, please follow the link and comment on their blog: pacha.dev/blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)