--- title: "High Performance Benchmarks" author: "Joseph Wood" date: "2026-03-04" output: prettydoc::html_pretty: theme: architect toc: true number_sections: false vignette: > %\VignetteIndexEntry{High Performance Benchmarks} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- This document serves as an overview for measuring the performance of `RcppAlgos` against other tools for generating combinations, permutations, and partitions. This stackoverflow post: [How to generate permutations or combinations of object in R?]() has some benchmarks. You will note that the examples in that post are relatively small. The benchmarks below will focus on larger examples where performance really matters and for this reason we only consider the packages [arrangements](), [partitions](), and [RcppAlgos](). ## Setup Information For the benchmarks below, we used a `2025 Macbook Air Apple M4 24 GB` machine. ``` r library(RcppAlgos) library(partitions) library(arrangements) #> #> Attaching package: 'arrangements' #> The following object is masked from 'package:partitions': #> #> compositions library(microbenchmark) options(digits = 4) options(width = 90) pertinent_output <- capture.output(sessionInfo()) cat(paste(pertinent_output[1:3], collapse = "\n")) #> R version 4.5.2 (2025-10-31) #> Platform: aarch64-apple-darwin20 #> Running under: macOS Sequoia 15.7.4 pkgs <- c("RcppAlgos", "arrangements", "partitions", "microbenchmark") sapply(pkgs, packageVersion, simplify = FALSE) #> $RcppAlgos #> [1] '2.10.0' #> #> $arrangements #> [1] '1.1.9' #> #> $partitions #> [1] '1.10.9' #> #> $microbenchmark #> [1] '1.5.0' numThreads <- min(as.integer(RcppAlgos::stdThreadMax() / 2), 6) numThreads #> [1] 5 ``` ## Combinations ### Combinations - Distinct ``` r set.seed(13) v1 <- sort(sample(100, 30)) m <- 21 t1 <- comboGeneral(v1, m, Parallel = T) t2 <- combinations(v1, m) stopifnot(identical(t1, t2)) dim(t1) #> [1] 14307150 21 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = comboGeneral(v1, m, nThreads = numThreads), cbRcppAlgosSer = comboGeneral(v1, m), cbArrangements = combinations(v1, m), times = 15, unit = "relative") #> Warning in microbenchmark(cbRcppAlgosPar = comboGeneral(v1, m, nThreads = numThreads), : #> less accurate nanosecond times to avoid potential integer overflows #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 15 #> cbRcppAlgosSer 3.360 3.111 3.024 3.055 3.027 2.695 15 #> cbArrangements 3.371 3.150 3.053 3.092 3.062 2.708 15 ``` ### Combinations - Repetition ``` r v2 <- v1[1:10] m <- 20 t1 <- comboGeneral(v2, m, repetition = TRUE, nThreads = numThreads) t2 <- combinations(v2, m, replace = TRUE) stopifnot(identical(t1, t2)) dim(t1) #> [1] 10015005 20 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = comboGeneral(v2, m, TRUE, nThreads = numThreads), cbRcppAlgosSer = comboGeneral(v2, m, TRUE), cbArrangements = combinations(v2, m, replace = TRUE), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 15 #> cbRcppAlgosSer 3.119 2.993 2.956 2.986 2.940 2.689 15 #> cbArrangements 2.963 2.895 2.857 2.882 2.829 2.722 15 ``` ### Combinations - Multisets ``` r myFreqs <- c(2, 4, 4, 5, 3, 2, 2, 2, 3, 4, 1, 4, 2, 5) v3 <- as.integer(c(1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610)) t1 <- comboGeneral(v3, 20, freqs = myFreqs, nThreads = numThreads) t2 <- combinations(freq = myFreqs, k = 20, x = v3) stopifnot(identical(t1, t2)) dim(t1) #> [1] 14594082 20 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = comboGeneral(v3, 20, freqs = myFreqs, nThreads = numThreads), cbRcppAlgosSer = comboGeneral(v3, 20, freqs = myFreqs), cbArrangements = combinations(freq = myFreqs, k = 20, x = v3), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 10 #> cbRcppAlgosSer 3.293 3.164 3.073 3.096 3.016 2.757 10 #> cbArrangements 6.804 6.579 6.332 6.421 6.241 5.418 10 ``` ## Permutations ### Permutations - Distinct ``` r v4 <- as.integer(c(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59)) t1 <- permuteGeneral(v4, 6, nThreads = numThreads) t2 <- permutations(v4, 6) stopifnot(identical(t1, t2)) dim(t1) #> [1] 8910720 6 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = permuteGeneral(v4, 6, nThreads = numThreads), cbRcppAlgosSer = permuteGeneral(v4, 6), cbArrangements = permutations(v4, 6), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 15 #> cbRcppAlgosSer 1.494 1.446 1.352 1.421 1.364 1.283 15 #> cbArrangements 2.177 2.110 2.032 2.081 2.135 1.793 15 ## Indexing permutation example with the partitions package t1 <- permuteGeneral(11, nThreads = 4) t2 <- permutations(11) t3 <- perms(11) dim(t1) #> [1] 39916800 11 stopifnot(identical(t1, t2), identical(t1, t(as.matrix(t3)))) rm(t1, t2, t3) invisible(gc()) microbenchmark(cbRcppAlgosPar = permuteGeneral(11, nThreads = 4), cbRcppAlgosSer = permuteGeneral(11), cbArrangements = permutations(11), cbPartitions = perms(11), times = 5, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 5 #> cbRcppAlgosSer 2.223 2.538 2.263 2.528 1.879 2.312 5 #> cbArrangements 3.537 3.568 3.267 3.432 2.610 3.430 5 #> cbPartitions 9.250 9.446 7.978 9.493 6.656 6.588 5 ``` ### Permutations - Repetition ``` r v5 <- v3[1:5] t1 <- permuteGeneral(v5, 10, repetition = TRUE, nThreads = numThreads) t2 <- permutations(v5, 10, replace = TRUE) stopifnot(identical(t1, t2)) dim(t1) #> [1] 9765625 10 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = permuteGeneral(v5, 10, TRUE, nThreads = numThreads), cbRcppAlgosSer = permuteGeneral(v5, 10, TRUE), cbArrangements = permutations(x = v5, k = 10, replace = TRUE), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.0000 10 #> cbRcppAlgosSer 2.423 2.355 1.997 2.327 2.159 0.9621 10 #> cbArrangements 2.822 2.768 2.494 2.712 2.521 1.8244 10 ``` ### Permutations - Multisets ``` r v6 <- sort(runif(12)) t1 <- permuteGeneral(v6, 7, freqs = rep(1:3, 4), nThreads = numThreads) t2 <- permutations(freq = rep(1:3, 4), k = 7, x = v6) stopifnot(identical(t1, t2)) dim(t1) #> [1] 19520760 7 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = permuteGeneral(v6, 7, freqs = rep(1:3, 4), nThreads = numThreads), cbRcppAlgosSer = permuteGeneral(v6, 7, freqs = rep(1:3, 4)), cbArrangements = permutations(freq = rep(1:3, 4), k = 7, x = v6), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 10 #> cbRcppAlgosSer 3.473 3.511 3.153 3.367 2.552 2.731 10 #> cbArrangements 3.937 3.985 3.658 3.874 3.138 3.117 10 ``` ## Partitions ### Partitions - Distinct #### All Distinct Partitions ``` r t1 <- comboGeneral(0:140, freqs=c(140, rep(1, 140)), constraintFun = "sum", comparisonFun = "==", limitConstraints = 140) t2 <- partitions(140, distinct = TRUE) t3 <- diffparts(140) # Each package has different output formats... we only examine dimensions # and that each result is a partition of 140 stopifnot(identical(dim(t1), dim(t2)), identical(dim(t1), dim(t(t3))), all(rowSums(t1) == 140), all(rowSums(t2) == 140), all(colSums(t3) == 140)) dim(t1) #> [1] 9617150 16 rm(t1, t2, t3) invisible(gc()) microbenchmark(cbRcppAlgosPar = partitionsGeneral(0:140, freqs=c(140, rep(1, 140)), nThreads = numThreads), cbRcppAlgosSer = partitionsGeneral(0:140, freqs=c(140, rep(1, 140))), cbArrangements = partitions(140, distinct = TRUE), cbPartitions = diffparts(140), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 10 #> cbRcppAlgosSer 3.252 3.133 2.911 2.982 2.739 2.709 10 #> cbArrangements 2.492 2.431 2.256 2.292 2.168 2.146 10 #> cbPartitions 17.359 17.199 15.909 17.115 14.684 13.572 10 ``` #### Restricted Distinct Partitions ``` r t1 <- comboGeneral(160, 10, constraintFun = "sum", comparisonFun = "==", limitConstraints = 160) t2 <- partitions(160, 10, distinct = TRUE) stopifnot(identical(t1, t2)) dim(t1) #> [1] 8942920 10 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = partitionsGeneral(160, 10, nThreads = numThreads), cbRcppAlgosSer = partitionsGeneral(160, 10), cbArrangements = partitions(160, 10, distinct = TRUE), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 10 #> cbRcppAlgosSer 3.153 3.082 2.713 3.017 2.973 1.902 10 #> cbArrangements 4.490 4.400 3.700 4.303 4.192 2.230 10 ``` ### Partitions - Repetition #### All Partitions ``` r t1 <- comboGeneral(0:65, repetition = TRUE, constraintFun = "sum", comparisonFun = "==", limitConstraints = 65) t2 <- partitions(65) t3 <- parts(65) # Each package has different output formats... we only examine dimensions # and that each result is a partition of 65 stopifnot(identical(dim(t1), dim(t2)), identical(dim(t1), dim(t(t3))), all(rowSums(t1) == 65), all(rowSums(t2) == 65), all(colSums(t3) == 65)) dim(t1) #> [1] 2012558 65 rm(t1, t2, t3) invisible(gc()) microbenchmark(cbRcppAlgosPar = partitionsGeneral(0:65, repetition = TRUE, nThreads = numThreads), cbRcppAlgosSer = partitionsGeneral(0:65, repetition = TRUE), cbArrangements = partitions(65), cbPartitions = parts(65), times = 20, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 20 #> cbRcppAlgosSer 2.793 2.749 2.276 2.415 2.182 1.776 20 #> cbArrangements 2.011 1.961 1.628 1.746 1.644 1.076 20 #> cbPartitions 9.885 9.768 7.711 8.600 7.274 4.866 20 ``` #### Restricted Partitions ``` r t1 <- comboGeneral(100, 15, TRUE, constraintFun = "sum", comparisonFun = "==", limitConstraints = 100) t2 <- partitions(100, 15) stopifnot(identical(t1, t2)) dim(t1) #> [1] 9921212 15 rm(t1, t2) # This takes a really long time... not because of restrictedparts, # but because apply is not that fast. This transformation is # needed for proper comparisons. As a result, we will compare # a smaller example # t3 <- t(apply(as.matrix(restrictedparts(100, 15, include.zero = F)), 2, sort)) t3 <- t(apply(as.matrix(restrictedparts(50, 15, include.zero = F)), 2, sort)) stopifnot(identical(partitions(50, 15), t3)) rm(t3) invisible(gc()) microbenchmark(cbRcppAlgosPar = partitionsGeneral(100, 15, TRUE, nThreads = numThreads), cbRcppAlgosSer = partitionsGeneral(100, 15, TRUE), cbArrangements = partitions(100, 15), cbPartitions = restrictedparts(100, 15, include.zero = FALSE), times = 10, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.00 1.000 1.000 1.000 1.000 10 #> cbRcppAlgosSer 3.378 3.31 2.974 2.952 2.735 2.638 10 #> cbArrangements 4.484 4.44 4.079 4.091 3.653 4.024 10 #> cbPartitions 15.181 15.09 13.452 13.576 12.017 11.979 10 ``` ### Partitions - Multisets Currently, `RcppAlgos` is the only package capable of efficiently generating partitions of multisets. Therefore, we will only time `RcppAlgos` and use this as a reference for future improvements. ``` r t1 <- comboGeneral(120, 10, freqs = rep(1:8, 15), constraintFun = "sum", comparisonFun = "==", limitConstraints = 120) dim(t1) #> [1] 7340225 10 stopifnot(all(rowSums(t1) == 120)) microbenchmark(cbRcppAlgos = partitionsGeneral(120, 10, freqs = rep(1:8, 15)), times = 10) #> Unit: milliseconds #> expr min lq mean median uq max neval #> cbRcppAlgos 178 180.3 182.4 181.4 182.9 189.6 10 ``` ## Compositions ### Compositions - Repetition #### All Compositions (Small case) ``` r t1 <- compositionsGeneral(0:15, repetition = TRUE) t2 <- arrangements::compositions(15) t3 <- partitions::compositions(15) # Each package has different output formats... we only examine dimensions # and that each result is a partition of 15 stopifnot(identical(dim(t1), dim(t2)), identical(dim(t1), dim(t(t3))), all(rowSums(t1) == 15), all(rowSums(t2) == 15), all(colSums(t3) == 15)) dim(t1) #> [1] 16384 15 rm(t1, t2, t3) invisible(gc()) microbenchmark(cbRcppAlgosSer = compositionsGeneral(0:15, repetition = TRUE), cbArrangements = arrangements::compositions(15), cbPartitions = partitions::compositions(15), times = 20, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosSer 1.107 1.101 1.098 1.096 1.094 1.053 20 #> cbArrangements 1.000 1.000 1.000 1.000 1.000 1.000 20 #> cbPartitions 110.419 117.834 145.577 145.641 165.113 191.249 20 ``` For the next two examples, we will exclude the `partitions` package for efficiency reasons. #### All Compositions (Larger case) ``` r t1 <- compositionsGeneral(0:23, repetition = TRUE) t2 <- arrangements::compositions(23) # Each package has different output formats... we only examine dimensions # and that each result is a partition of 23 stopifnot(identical(dim(t1), dim(t2)), all(rowSums(t1) == 23), all(rowSums(t2) == 23)) dim(t1) #> [1] 4194304 23 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = compositionsGeneral(0:23, repetition = TRUE, nThreads = numThreads), cbRcppAlgosSer = compositionsGeneral(0:23, repetition = TRUE), cbArrangements = arrangements::compositions(23), times = 20, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 20 #> cbRcppAlgosSer 3.833 3.639 3.562 3.580 3.526 3.051 20 #> cbArrangements 4.138 3.921 3.831 3.853 3.783 3.250 20 ``` #### Compositions of Specific Length ``` r t1 <- compositionsGeneral(30, 10, repetition = TRUE) t2 <- arrangements::compositions(30, 10) stopifnot(identical(t1, t2), all(rowSums(t1) == 30)) dim(t1) #> [1] 10015005 10 rm(t1, t2) invisible(gc()) microbenchmark(cbRcppAlgosPar = compositionsGeneral(30, 10, repetition = TRUE, nThreads = numThreads), cbRcppAlgosSer = compositionsGeneral(30, 10, repetition = TRUE), cbArrangements = arrangements::compositions(30, 10), times = 20, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgosPar 1.000 1.000 1.000 1.000 1.000 1.000 20 #> cbRcppAlgosSer 3.075 2.986 2.908 2.874 2.845 2.780 20 #> cbArrangements 3.133 2.992 2.945 2.891 2.861 3.108 20 ``` ### Specialized Composition Benchmarks Similar to partitions of multisets, several composition variants supported in `RcppAlgos` (e.g., distinct parts, capped cases, etc.) are not currently available in other R packages or combinatorics libraries. In such cases, benchmarks will only report timings for `RcppAlgos`, which can serve as a reference point for future implementations and improvements. #### Compositions with Specific `target` ``` r t1 <- compositionsGeneral(10, 10, repetition = TRUE, target = 30) dim(t1) #> [1] 9091270 10 stopifnot(all(rowSums(t1) == 30)) microbenchmark( cbRcppAlgosSer = compositionsGeneral(10, 10, repetition = TRUE, target = 30), cbRcppAlgosPar = compositionsGeneral(10, 10, repetition = TRUE, target = 30, nThreads = numThreads), times = 10, check = "identical" ) #> Unit: milliseconds #> expr min lq mean median uq max neval #> cbRcppAlgosSer 57.44 57.49 59.77 59.17 61.68 63.69 10 #> cbRcppAlgosPar 17.49 17.90 23.16 21.83 25.34 39.69 10 ``` #### Compositions with Distinct Parts ``` r t1 <- compositionsGeneral(50, 8) dim(t1) #> [1] 4677120 8 stopifnot(all(rowSums(t1) == 50)) microbenchmark( cbRcppAlgosSer = compositionsGeneral(50, 8), cbRcppAlgosPar = compositionsGeneral(50, 8, nThreads = numThreads), times = 10, check = "identical" ) #> Unit: milliseconds #> expr min lq mean median uq max neval #> cbRcppAlgosSer 214.49 215.25 216.22 215.53 215.99 221.19 10 #> cbRcppAlgosPar 52.13 53.51 55.81 55.12 56.63 64.84 10 ``` #### Compositions with Distinct Parts & Specific `target` ``` r t1 <- compositionsGeneral(30, 7, target = 60) dim(t1) #> [1] 11773440 7 stopifnot(all(rowSums(t1) == 60)) microbenchmark( cbRcppAlgosSer = compositionsGeneral(30, 7, target = 60), cbRcppAlgosPar = compositionsGeneral(30, 7, target = 60, nThreads = numThreads), times = 10, check = "identical" ) #> Unit: milliseconds #> expr min lq mean median uq max neval #> cbRcppAlgosSer 359.1 359.62 363.92 361.99 364.36 380.2 10 #> cbRcppAlgosPar 92.9 94.45 96.98 96.17 97.18 105.7 10 ``` ## Iterators We will show one example from each category to demonstrate the efficiency of the iterators in `RcppAlgos`. The results are similar for the rest of the cases not shown. ### Combinations ``` r pkg_arrangements <- function(n, total) { a <- icombinations(n, as.integer(n / 2)) for (i in 1:total) a$getnext() } pkg_RcppAlgos <- function(n, total) { a <- comboIter(n, as.integer(n / 2)) for (i in 1:total) a@nextIter() } total <- comboCount(18, 9) total #> [1] 48620 microbenchmark(cbRcppAlgos = pkg_RcppAlgos(18, total), cbArrangements = pkg_arrangements(18, total), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgos 1.00 1.0 1.00 1.00 1.00 1.00 15 #> cbArrangements 19.78 19.5 19.13 19.27 18.76 18.57 15 ``` ### Permutations ``` r pkg_arrangements <- function(n, total) { a <- ipermutations(n) for (i in 1:total) a$getnext() } pkg_RcppAlgos <- function(n, total) { a <- permuteIter(n) for (i in 1:total) a@nextIter() } total <- permuteCount(8) total #> [1] 40320 microbenchmark(cbRcppAlgos = pkg_RcppAlgos(8, total), cbArrangements = pkg_arrangements(8, total), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgos 1.00 1.00 1.00 1.00 1.00 1.00 15 #> cbArrangements 19.42 19.19 18.87 18.76 18.38 19.43 15 ``` ### Partitions ``` r pkg_partitions <- function(n, total) { a <- firstpart(n) for (i in 1:(total - 1)) a <- nextpart(a) } pkg_arrangements <- function(n, total) { a <- ipartitions(n) for (i in 1:total) a$getnext() } pkg_RcppAlgos <- function(n, total) { a <- partitionsIter(0:n, repetition = TRUE) for (i in 1:total) a@nextIter() } total <- partitionsCount(0:40, repetition = TRUE) total #> [1] 37338 microbenchmark(cbRcppAlgos = pkg_RcppAlgos(40, total), cbArrangements = pkg_arrangements(40, total), cbPartitions = pkg_partitions(40, total), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgos 1.00 1.00 1.00 1.00 1.00 1.00 15 #> cbArrangements 15.38 15.19 14.39 15.04 13.39 13.75 15 #> cbPartitions 24.48 24.25 22.94 23.91 21.30 21.55 15 ``` ### Compositions ``` r pkg_partitions <- function(n, total) { a <- firstcomposition(n) for (i in 1:(total - 1)) a <- nextcomposition(a, FALSE) } pkg_arrangements <- function(n, total) { a <- icompositions(n) for (i in 1:total) a$getnext() } pkg_RcppAlgos <- function(n, total) { a <- compositionsIter(0:n, repetition = TRUE) for (i in 1:total) a@nextIter() } total <- compositionsCount(0:15, repetition = TRUE) total #> [1] 16384 microbenchmark(cbRcppAlgos = pkg_RcppAlgos(15, total), cbArrangements = pkg_arrangements(15, total), cbPartitions = pkg_partitions(15, total), times = 15, unit = "relative") #> Unit: relative #> expr min lq mean median uq max neval #> cbRcppAlgos 1.00 1.00 1.0 1.00 1.00 1.00 15 #> cbArrangements 14.35 14.16 13.8 14.01 13.81 12.46 15 #> cbPartitions 44.92 44.54 43.8 44.08 43.62 41.54 15 ```