The following compares available options for numerical integration to
demonstrate the simplicity of integratecpp
’s C++11 wrapper
for R’s C API to its numerical integration routines.
The Problem
We are using the following simple integral to compare various numerical integration options:
\[ \int_{0}^{1} 1 / x^{0.7} \mathrm{d}x . \]
Using base R
We begin by calculating the integral using base-R.
- Finite-interval integration uses a 21-point Gauss-Kronrod rule on equally-spaced subintervals.
- Infinite-interval integration uses a 15-point Gauss-Kronrod rule on equally-spaced subintervals after transforming the integrand to the interval \([0, 1]\).
- Both use Wynn’s \(\epsilon\) algorithm for limit-extrapolation.
It is possible to configure the integration algorithms by changing the requested relative and absolute accuracies and the maximum number of subintervals. All of these features are shared by the following approaches. However, some have additional features.
# R
integrate_R <- function() { # nolint
integrate(
function(x) 1 / x^0.7,
lower = 0, upper = 1
) |>
(function(x) {
list(
"value" = x$value,
"abserr" = x$abs.err,
"subdivisions" = x$subdivisions
)
})()
}
integrate_R()
#> $value
#> [1] 3.333333
#>
#> $abserr
#> [1] 7.238654e-14
#>
#> $subdivisions
#> [1] 6
Using R’s C-API
We can also calculate the same integral using R’s C-API:
// C++11
#include <math.h>
#include <R_ext/Applic.h>
#include <Rcpp.h>
void fn(double *x, int n, void* ex) {
for (int i = 0; i < n; ++i) {
x[i] = 1. / pow(x[i], 0.7);
}
return;
}
// [[Rcpp::export(rng=false)]]
Rcpp::List integrate_Cish() {
double lower = 0.;
double upper = 1.;
double result;
double abserr;
int last;
int neval;
int limit = 100;
double epsrel = 0.0001220703125;
double epsabs = epsrel;
int lenw = 4 * limit;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wvla-extension"
int iwork[limit]; // C99
double work[lenw]; // C99
#pragma GCC diagnostic pop
int ier = 0;
// NOTE: we skip guarding against exceptions thrown from integrand since
// it will not throw exceptions when workspace is configured properly
Rdqags(&fn, nullptr, &lower, &upper, &epsabs, &epsrel, &result,
&abserr, &neval, &ier, &limit, &lenw, &last, iwork, work
);
return Rcpp::List::create(
Rcpp::Named("value") = result,
Rcpp::Named("abserr") = abserr,
Rcpp::Named("subdivisions") = last
);
}
# R
integrate_Cish()
#> $value
#> [1] 3.333333
#>
#> $abserr
#> [1] 7.238654e-14
#>
#> $subdivisions
#> [1] 6
Using integratecpp
Using our C++11 wrapper, numerical integration can be done seamlessly in Rcpp:
// C++11
// [[Rcpp::plugins(cpp11)]]
// [[Rcpp::depends(integratecpp)]]
#include <cmath>
#include <Rcpp.h>
#include <integratecpp.h>
// [[Rcpp::export(rng=false)]]
Rcpp::List integrate_integratecpp() {
const auto fn = [](const double x) {
return 1. / std::pow(x, 0.7);
};
const auto result = integratecpp::integrate(fn, 0., 1.);
return Rcpp::List::create(
Rcpp::Named("value") = result.value,
Rcpp::Named("abserr") = result.absolute_error,
Rcpp::Named("subdivisions") = result.subdivisions
);
}
# R
integrate_integratecpp()
#> $value
#> [1] 3.333333
#>
#> $abserr
#> [1] 7.238654e-14
#>
#> $subdivisions
#> [1] 6
Using RcppGSL
With RcppGSL
, we can use gsl
in a similar
way as R’s C-API:
// C++11
// [[Rcpp::depends(RcppGSL)]]
#include <math.h>
#include <Rcpp.h>
#include <RcppGSL.h>
#include <gsl/gsl_integration.h>
double fn(double x, void* params) {
return 1. / pow(x, 0.7);
}
// [[Rcpp::export(rng=false)]]
Rcpp::List integrate_RcppGSL() {
double lower = 0.;
double upper = 1.;
double result;
double abserr;
size_t limit = 100;
double epsrel = 0.0001220703125;
double epsabs = epsrel;
gsl_function f;
f.function = &fn;
f.params = nullptr;
gsl_integration_workspace *w =
gsl_integration_workspace_alloc(limit);
gsl_integration_qags(
&f, lower, upper, epsabs, epsrel, limit, w, &result, &abserr
);
int subintervals = w->size;
gsl_integration_workspace_free(w);
return Rcpp::List::create(
Rcpp::Named("value") = result,
Rcpp::Named("abserr") = abserr,
Rcpp::Named("subdivisions") = subintervals
);
}
# R
integrate_RcppGSL()
#> $value
#> [1] 3.333333
#>
#> $abserr
#> [1] 7.238654e-14
#>
#> $subdivisions
#> [1] 6
Using RcppNumerical
With RcppNumerical
, we must define the integrator in the
Numer
namespace:
// C++11
// [[Rcpp::depends(RcppEigen)]]
// [[Rcpp::depends(RcppNumerical)]]
#include <cmath>
#include <RcppNumerical.h>
namespace Numer {
class Fn : public Func {
double operator()(const double& x) const {
return 1. / std::pow(x, 0.7);
}
};
} // Numer
// [[Rcpp::export]]
Rcpp::List integrate_RcppNumerical() {
double lower = 0.;
double upper = 1.;
size_t limit = 100;
double epsrel = 0.0001220703125;
double epsabs = epsrel;
Numer::Fn fn;
double abserr;
int ier;
const double res = Numer::integrate(
fn, lower, upper, abserr, ier, limit, epsabs, epsrel,
Numer::Integrator<double>::GaussKronrod21
);
return Rcpp::List::create(
Rcpp::Named("value") = res,
Rcpp::Named("abserr") = abserr
);
}
# R
integrate_RcppNumerical()
#> $value
#> [1] 3.333284
#>
#> $abserr
#> [1] 0.0003695186
Benchmark
A benchmarks shows that our wrapper is almost as fast as the native C-API:
# R
bench::mark(
integrate_R(),
integrate_integratecpp(),
integrate_Cish(),
integrate_RcppGSL(),
integrate_RcppNumerical(),
check = FALSE
)
#> # A tibble: 5 × 6
#> expression min median `itr/sec` mem_alloc `gc/sec`
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
#> 1 integrate_R() 34.62µs 37.45µs 26037. 69.74KB 20.8
#> 2 integrate_integratecpp() 5.53µs 5.67µs 174479. 5.17KB 0
#> 3 integrate_Cish() 5.21µs 5.28µs 186126. 4.12KB 0
#> 4 integrate_RcppGSL() 5.57µs 5.64µs 173514. 4.12KB 17.4
#> 5 integrate_RcppNumerical() 37.32µs 37.66µs 26380. 6.62KB 0