R Yoda
R Yoda

Reputation: 8750

Vectorized version of charToRaw with good performance

I want to get a vector of raw bytes from a vector of character (to apply an encryption function that requires raw bytes as input on all values of a data.table column).

charToRaw does not work vectorized but processes only the first element of a vector:

x <- c("hello", "my", "world")
charToRaw(x)
# Warning message:
# In charToRaw(x) : argument should be a character vector of length 1
# all but the first element will be ignored

Is there a vectorized version of charToRaw offering a good performance? Why does base R's version not offer a vectorized version?

I know I could use sapply or myapply but I would end up with an internal loop over all rows...

Edit 1: The result shall be a vector of the same size as 'x' with each element representing the raw bytes of the corresponding input element.

Edit 2 + 3: My result should look like this (eg. as list)

x.raw
[1] 68 65 6c 6c 6f
[2] 6d 79
[3] 77 6f 72 6c 64

The problem is that R doesn't seem to support a vector of raw since raw itself is like vector of bytes... Any idea how to solve this?

Edit 4 + 5:

I have bench-marked the current proposals:

library(microbenchmark)
x <- sample(c("hello", "my", "world"), 1E6, TRUE)
microbenchmark::microbenchmark(
  sapply_loop  = sapply(x, charToRaw),
  lapply_loop = lapply(x, charToRaw),
  vectorize_loop = { charToRawVec <-Vectorize(charToRaw, "x")
                     charToRawVec(x) },
  split = split(charToRaw(paste(x, collapse = "")), rep(seq_len(length(x)), nchar(x))),
  charToRaw_with_cpp = charToRaw_cpp(x),
  times = 5
)

The Rcpp solution from the answer of @Brian is 4 to 5 times faster than all other proposals (depending on the length of the strings):

Unit: milliseconds
               expr       min        lq      mean    median        uq       max neval
        sapply_loop  761.6041 1149.7972 1153.1992 1202.6303 1306.2110 1345.7531     5
        lapply_loop  950.5337  972.1374 1172.4354 1134.9821 1300.4941 1504.0297     5
     vectorize_loop  951.9297  983.2725 1134.0204 1147.1145 1250.9649 1336.8201     5
              split 1201.5009 1275.7123 1409.3622 1425.0124 1529.5082 1615.0772     5
 charToRaw_with_cpp  111.7791  113.1815  313.5623  384.7327  466.9929  491.1253     5

Upvotes: 2

Views: 493

Answers (2)

Brian
Brian

Reputation: 8275

This is a version that uses the internal C source for charToRaw without any of the error checking. The loop in Rcpp should be as fast as you can get, although I don't know if there's a better way to handle the memory allocation. As you can see, you don't get a statistically significant performance bump over purrr::map, but it is better than sapply.

library(Rcpp)

Rcpp::cppFunction('List charToRaw_cpp(CharacterVector x) {
  int n = x.size();
  List l = List(n);

  for (int i = 0; i < n; ++i) {
    int nc = LENGTH(x[i]);
    RawVector ans = RawVector(nc);
    memcpy(RAW(ans), CHAR(x[i]), nc);
    l[i] = ans;
  }
  return l;
}')

# Random vector of 5000 strings of 5000 characters each
x <- unlist(purrr::rerun(5000, stringr::str_c(sample(c(letters, LETTERS), 5000, replace = T), collapse = "")))

microbenchmark::microbenchmark(
  sapply(x, charToRaw),
  purrr::map(x, charToRaw),
  charToRaw_cpp(x)
)
Unit: milliseconds
                    expr       min        lq      mean    median       uq       max neval cld
    sapply(x, charToRaw) 60.337729 69.313684 76.908557 73.232365 78.99251 398.00732   100   b
purrr::map(x, charToRaw)  8.849688  9.201125 17.117435  9.376843 10.09294 292.74068   100  a 
        charToRaw_cpp(x)  5.578212  5.827794  7.998507  6.151864  7.10292  23.81905   100  a

With 1000 iterations you start to see an effect:

Unit: milliseconds
                    expr      min       lq      mean   median        uq      max neval cld
purrr::map(x, charToRaw) 8.773802 9.191173 13.674963 9.425828 10.602676 302.7293  1000   b
        charToRaw_cpp(x) 5.591585 5.868381  9.370648 6.119673  7.445649 295.1833  1000  a

Edited note on performance:

I assumed you would see a bigger difference in performance with larger strings and vectors. But actually the biggest difference so far is for a 50-length vector of 50-character strings:

Unit: microseconds
                       expr    min     lq     mean median      uq     max neval cld
       sapply(x, charToRaw) 66.245 69.045 77.44593 70.288 72.4650 862.110   500   b
   purrr::map(x, charToRaw) 65.313 68.733 75.85236 70.599 72.7765 621.392   500   b
          charToRaw_cpp(x)  4.666  6.221  7.47512  6.844  7.7770  58.159   500  a

Upvotes: 2

Brigadeiro
Brigadeiro

Reputation: 2945

You can use Vectorize() to complete this task:

x <- c("hello", "my", "world")
charToRawVec <- Vectorize(FUN = charToRaw, vectorize.args = "x")
charToRawVec(x)

Upvotes: 1

Related Questions