1
\$\begingroup\$

I have the following C++ code snippet that randomly samples a single row in an array called pop with num_specs columns and perms rows. In addition, K = 1. The triply-nested for loop uses a pointer for referencing.

Some of the below syntax (such as IntegerVector) is from Rcpp, an R package to integrate C++ code with R code.

// [[Rcpp::depends(RcppArmadillo)]]
// [[Rcpp::depends(RcppProgress)]]
#define ARMA_DONT_PRINT_OPENMP_WARNING
#include <RcppArmadillo.h>
#include <RcppArmadilloExtensions/sample.h>
#include <set>
using namespace Rcpp;

int sample_one(int n) {
  return n * unif_rand();
} 

int sample_n_distinct(const IntegerVector& x, 
                      int k,
                      const int * pop_ptr) {

IntegerVector ind_index = RcppArmadillo::sample(x, k, false);
std::set<int> distinct_container;

for (int i = 0; i < k; i++) {
    distinct_container.insert(pop_ptr[ind_index[i]]);
}

return distinct_container.size();
}

// [[Rcpp::export]]
arma::Cube<int> accumulate(const arma::Cube<int>& pop,
                     const IntegerVector& specs,
                     int perms,
                     int K) {

int num_specs = specs.size();
arma::Cube<int> res(perms, num_specs, K);

IntegerVector specs_C = specs - 1;
const int * pop_ptr;
int i, j, k;

for (i = 0; i < K; i++) {
    for (k = 0; k < num_specs; k++) {
        for (j = 0; j < perms; j++) {
            pop_ptr = &(pop(sample_one(perms), 0, sample_one(K)));
            res(j, k, i) = sample_n_distinct(specs_C, k + 1, pop_ptr);
        }
    }
}
return res;
}

While loops in compiled languages aren't bad, it is possible to write slow code.

I'm not a native C++ programmer, so I don't know all the tricks of the trade.

Is there a way to reduce the number of levels in the triply-nested for loop above, possibly by employing modular arithmetic in order to see a gain in speed for large input values?

The R code is below:

## Set up container to hold the identity of each individual from each permutation ##
num.specs <- N

## Create an ID for each tag ##
tags <- 1:h

## Assign individuals (N) ##
specs <- 1:num.specs

## Generate permutations. Assume each permutation has N individuals, and sample those 
# individuals' tags from the probabilities ##
  gen.perms <- function() {
      sample(tags, size = num.specs, replace = TRUE, prob = probs)
  }

  pop <- array(dim = c(perms, num.specs, K))

  for (i in 1:K) {
    pop[,, i] <- replicate(perms, gen.perms())
  }

## Perform accumulation ##
HAC.mat <- accumulate(pop, specs, perms, K)

## Example
K <- 1
N <- 100
h <- 5
probs <- rep(1/h, h)
perms <- 100
\$\endgroup\$
8
  • 2
    \$\begingroup\$ This question is incomplete. To help reviewers give you better answers, please add sufficient context to your question. The more you tell us about what your code does and what the purpose of doing that is, the easier it will be for reviewers to help you. The current title states your concerns about the code; it needs an edit to simply state the task; see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ Commented Oct 3, 2018 at 17:30
  • \$\begingroup\$ @TobySpeight Thanks! I've added further detail to the title and post, which I hope is sufficient. Could you take a look? \$\endgroup\$ Commented Oct 3, 2018 at 17:43
  • \$\begingroup\$ @JarrettPhillips, let me add my two cents. The first problem I see is the absence of purpose: it should be clear why the code does what it does. Second: where does the array come from? What those functions are? Ideally code in question should be compilable and runnable, and output correct result. I know it is not always possible to do that, but at this stage it is hard to give effective review. \$\endgroup\$ Commented Oct 3, 2018 at 17:57
  • \$\begingroup\$ @TobySpeight Understandable. I've now added the full code, which is written with the purpose of being integrated with R via the Rcpp package. I've also changed the title to be a bit more informative. \$\endgroup\$ Commented Oct 3, 2018 at 18:16
  • 1
    \$\begingroup\$ That's looking quite a bit better now (and I've withdrawn my close-vote). You could improve the title a bit: focus on the high-level goal, rather than the mechanism. The title really should go with the motivation part of the explanation (which is still very thin), rather than mechanism - see How to Ask and the "How to get the best value" link from my first comment. With that fixed up, you'll be good to go! I hope you get some helpful answers; unfortunately, I've never used R, so that part is beyond my capabilities, but I might be able to look at the C++ now you've explained the purpose. \$\endgroup\$ Commented Oct 4, 2018 at 13:30

1 Answer 1

4
\$\begingroup\$

I can only comment on the C++ code, as I've never used R nor Rcpp.

For an ignorant reader such as myself, it would have helped to omit the using namespace Rcpp;, so that I could see which names come from that library (I did quite a bit of external reading to even understand the code).

Assuming I'm right that sample() returns an array of length k, then we can use range-based for to iterate through its values (since IntegerVector has suitable begin() and end()):

// sample k values from x, without replacement
const Rcpp::IntegerVector ind_index = Rcpp::RcppArmadillo::sample(x, k, false);

// how many different values do they index in pop_ptr?
std::set<int> distinct_container;
for (int i: ind_index) {
    distinct_container.insert(pop_ptr[i]);
}

In accumulate() (which name I dislike, because it sounds like std::accumulate()), we can reduce the scope of i, j, k and pop_ptr quite simply:

for (int i = 0; i < K; ++i) {
    for (int k = 0;  k < num_specs;  ++k) {
        for (int j = 0;  j < perms;  ++j) {
            const int *const pop_ptr = &(pop(sample_one(perms), 0, sample_one(K)));
            res(j, k, i) = sample_n_distinct(specs_C, k + 1, pop_ptr);
        }
    }
}

Also, I'd be inclined to show that the creation of specs_C is more than just a conversion from specs - 1 - that's actually a constructor argument specifying the length of the new vector:

IntegerVector specs_C(specs - 1);

I don't see any code that ever modifies this vector; if it's supposed to remain full of zeros, then it can be declared const.


With those changes, and an automated re-indent, we get something like this for those two functions (I've compiled, but not tested this):

int sample_one(int n)
{
    return n * R::unif_rand();
}

int sample_n_distinct(const Rcpp::IntegerVector& x,
                      int k,
                      const int * pop_ptr)
{
    // sample k values from x, without replacement
    const auto ind_index = Rcpp::RcppArmadillo::sample(x, k, false);

    // how many different values do they index in pop_ptr?
    std::set<int> distinct_container;
    for (int i: ind_index) {
        distinct_container.insert(pop_ptr[i]);
    }

    return distinct_container.size();
}

// [[Rcpp::export]]
arma::Cube<int> accumulate(const arma::Cube<int>& pop,
                           const Rcpp::IntegerVector& specs,
                           int perms,
                           int K)
{
    auto const num_specs = specs.size();
    arma::Cube<int> res(perms, num_specs, K);

    Rcpp::IntegerVector specs_C(specs - 1);

    for (int i = 0;  i < K;  ++i) {
        for (int k = 0;  k < num_specs; ++k) {
            for (int j = 0;  j < perms; ++j) {
                const auto& sampled = pop(sample_one(perms), 0, sample_one(K));
                res(j, k, i) = sample_n_distinct(specs_C, k + 1, &sampled);
            }
        }
    }
    return res;
}

Sadly, I don't have the domain knowledge to suggest more meaningful improvements other than these fairly mechanical changes that improve readability.

In particular, I don't understand why we have to pass specs_C to accumulate() instead of sampling directly from pop_ptr.

I'm also very wary of taking the address of the element returned by pop() - that seems to assume a memory layout that could vary between platforms, for instance. I'd expect to use one of the subcube view operations to construct the population:

int sample_n_distinct(const Rcpp::IntegerVector& population, int k)
{
    // sample k values from population, without replacement
    const auto sampled = Rcpp::RcppArmadillo::sample(population, k, false);

    // how many different values do have?
    std::set<int> distinct(sampled.begin(), sampled.end());

    return distinct.size();
}

// [[Rcpp::export]]
arma::Cube<int> accumulate(const arma::Cube<int>& pop,
                           const Rcpp::IntegerVector& specs,
                           int perms,
                           int K)
{
    auto const num_specs = specs.size();
    arma::Cube<int> res(perms, num_specs, K);

    for (int i = 0;  i < K;  ++i) {
        for (int k = 0;  k < num_specs; ++k) {
            for (int j = 0;  j < perms; ++j) {
                const auto& sampled = pop.tube(sample_one(perms), sample_one(K));
                const auto sampled_vector
                    = Rcpp::IntegerVector(sampled.begin(), sampled.end());
                res(j, k, i) = sample_n_distinct(sampled_vector, k + 1);
            }
        }
    }
    return res;
}

I may be wrong on exactly what subcube view is required, so pop.tube() may need to be a different call, but you get the idea. See how much simpler life is when you can use iterators instead of grubbing around with pointers?

\$\endgroup\$
6
  • \$\begingroup\$ Well, it's syntactically correct; I don't have the unit tests to actually prove it gives the right results, but presumably you do, and can trivially swap implementations and test them. \$\endgroup\$ Commented Oct 4, 2018 at 16:23
  • \$\begingroup\$ Yes, I'll give it a run and let you know! I greatly appreciate the assistance! \$\endgroup\$ Commented Oct 4, 2018 at 16:35
  • \$\begingroup\$ Your first implementation works nicely! For your second implementation, I would need to instal C++11 \$\endgroup\$ Commented Oct 4, 2018 at 17:03
  • 1
    \$\begingroup\$ Ah - the question didn't mention that you were stuck with ancient version of C++! \$\endgroup\$ Commented Oct 4, 2018 at 17:27
  • \$\begingroup\$ Yes - I should have mentioned that. Do you think there will be a performance gain from the usage of auto, seeing that variable type is inferred only at compile time? I've heard different stories pertaining to this aspect. \$\endgroup\$ Commented Oct 5, 2018 at 20:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.