Improving string vector performance for push_back and subscript assignment

Open
#406 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Refactor
Clarity
Mostly clear
Activity status
Stale
Tech stack
cpp, r
Domain
performance

Research direction

Start in inst/include/cpp11/strings.hpp, especially the push_back implementation around line 133, and trace the subscript-assignment path for writable::strings. Reproduce the supplied assignment and push_back benchmarks, then compare behavior and performance after avoiding unnecessary std::string-to-r_string conversion and protection where the issue describes it.

Written by the indexing model from the issue text.

Description

When performing subscript assignment of a std::string to writable::strings there's first a conversion to the r_string class which includes a protection. However, that protection shouldn't be necessary if the result is immediately assigned and there is no translation.

Here's a benchmark compared to Rcpp, where I am generating random strings and assigning them to a pre-allocated vector.

microbenchmark(
  rcpp = assign_rcpp(n=1e5, -1),   # Assign random strings to Rcpp::CharacterVector
  cpp11 = assign_cpp11(n=1e5, -1), # Assign random strings to cpp11::writable::strings
  times = 20, setup = gc(full=TRUE))

Unit: milliseconds
  expr       min        lq      mean   median        uq       max neval
  rcpp  35.09063  36.53801  37.74236  37.3688  39.03084  41.29021    20
 cpp11 145.24590 148.24958 154.64878 151.7362 158.87924 177.13784    20

(The performance difference depends a lot on platform, but there's a 4x difference on a mac laptop.)

One possibility is to add some sort of overload or specialization for std::string for subscript assignment and push_back to short circuit the conversion to r_string.

Here's an example for push_back (https://github.com/traversc/cpp11/blob/main/inst/include/cpp11/strings.hpp#L133)

microbenchmark::microbenchmark(
  grow_strings_cpp11 = grow_strings_cpp11(1e5, -1),           # push_back 1e5 random strings, existing implementation
  grow_strings_fast_cpp11 = grow_strings_fast_cpp11(1e5, -1), # push_back_fast implementation
  grow_strings_manual = grow_strings_manual(1e5, -1),         # manual re-implementation using only SEXP
  times = 20, setup = gc(full=TRUE))

Unit: milliseconds
                    expr       min        lq      mean    median        uq       max neval
      grow_strings_cpp11 148.06594 152.42953 158.78716 156.06482 163.46553 183.88168    20
 grow_strings_fast_cpp11  38.22086  39.39721  40.47245  40.07249  41.43170  44.69328    20
     grow_strings_manual  38.65436  40.02137  41.00465  41.10390  41.81826  46.18455    20

assign_rcpp / assign_cpp11 benchmark functions

#include <random>
std::random_device rd;
std::mt19937 gen(rd());

int random_int(int min, int max) {
  std::uniform_int_distribution<int> dist(min, max);
  return dist(gen);
}

std::string random_string() {
  std::string s(10, '\0');
  for (size_t i = 0; i < 10; i++) {
    s[i] = random_int(0, 25) + 'a';
  }
  return s;
}

#include <Rcpp.h>
using namespace Rcpp;

# Rcpp
// [[Rcpp::export(rng=false)]]
CharacterVector assign_rcpp(size_t n, int seed) {
  CharacterVector x(n);
  if(seed != -1) gen.seed(seed);
  for (size_t i = 0; i < n; ++i) {
    x[i] = random_string();
  }
  return x;
}

#include "cpp11.hpp"
using namespace cpp11;
namespace writable = cpp11::writable;

# cpp11
[[cpp11::register]]
writable::strings assign_cpp11(size_t n, int seed) {
  if(seed != -1) gen.seed(seed);
  writable::strings x(n);
  for (size_t i = 0; i < n; ++i) {
    x[i] = random_string();
  }
  return x;
}

push_back benchmark functions

#include "cpp11.hpp"
#include <vector>
#include <random>
using namespace cpp11;
namespace writable = cpp11::writable;

std::random_device rd;
std::mt19937 gen(rd());

double random_double() {
  std::uniform_real_distribution<double> dist(0.0, 1.0);
  return dist(gen);
}

int random_int(int min, int max) {
  std::uniform_int_distribution<int> dist(min, max);
  return dist(gen);
}

std::string random_string() {
  std::string s(10, '\0');
  for (size_t i = 0; i < 10; i++) {
    s[i] = random_int(0, 25) + 'a';
  }
  return s;
}

[[cpp11::register]]
writable::strings grow_strings_cpp11(size_t n, int seed) {
  if(seed != -1) gen.seed(seed);
  writable::strings x;
  for (size_t i = 0; i < n; ++i) {
    x.push_back(random_string());
  }
  return x;
}

[[cpp11::register]]
writable::strings grow_strings_fast_cpp11(size_t n, int seed) {
  if(seed != -1) gen.seed(seed);
  writable::strings x;
  for (size_t i = 0; i < n; ++i) {
    x.push_back_fast(random_string());
  }
  return x;
}

[[cpp11::register]]
SEXP grow_strings_manual(size_t n, int seed) {
  if(seed != -1) gen.seed(seed);
  SEXP data_ = PROTECT(Rf_allocVector(STRSXP, 0));
  size_t size_ = 0;
  size_t capacity_ = 0;
  for (size_t i = 0; i < n; ++i) {
    if(size_ == capacity_) {
      capacity_ = capacity_ == 0 ? 1 : capacity_ * 2;
      SEXP new_data_ = PROTECT(Rf_allocVector(STRSXP, capacity_));
      for(size_t j = 0; j < size_; ++j) {
        SET_STRING_ELT(new_data_, j, STRING_ELT(data_, j));
      }
      UNPROTECT(2);
      data_ = PROTECT(new_data_);
    }
    SET_STRING_ELT(data_, size_++, Rf_mkChar(random_string().c_str()));
  }
  // copy back down to size
  if(size_ < capacity_) {
    SEXP new_data_ = PROTECT(Rf_allocVector(STRSXP, size_));
    for(size_t j = 0; j < size_; ++j) {
      SET_STRING_ELT(new_data_, j, STRING_ELT(data_, j));
    }
    UNPROTECT(2);
    return new_data_;
  } else {
    UNPROTECT(1);
    return data_;
  }
}
Dominant language
C++
Stars
224
Forks
52
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from r-lib/cpp11

All issues in r-lib/cpp11

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.