serializer is a package which demonstrates how to use R’s internal
serialization interface from C.
This package was developed to help me figure out the serialization process in R. It is perhaps only really interesting if you want to look at and/or steal the C code. It’s under the MIT license, so please feel free to re-use in your own projects.
marshall_raw()/unmarshall_raw() are direct analogues for
base::serialize() and base::unserialize()
marshall_file()/unmarshall_file() are the same as above but for
filesmarshall_con()/unmarshall_con() are the same as above but for
connections e.g. gzfile()
marshall_con_illegal()/unmarshall_con_illegal() are the same as
above but for connections, and using a non-API function internal to R.
R CMD check because of the non-API
methods.file() or gzfile() connection, these
functions are 5x to 50x faster than the “legal” way of
reading/writing with connections.calc_serialized_size() calculates the exact size of the serialized
representation of an object using R’s serialization infrastructure -
but without actually performing the serialization. This is an
extremely fast calculation.You can install from GitHub with:
# install.package('remotes')
# remotes::install_github('coolbutuseless/serializer')
install.packages("serializer", repos = c('https://coolbutuseless.r-universe.dev', 'https://cloud.r-project.org'))
library(serializer)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The object to be serialized
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dat <- head(mtcars, 3)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calculate exactly how many bytes this will take once serialized
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
serializer::calc_serialized_size(dat)
#> [1] 674
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Serialized results from this package and base::serialize should be identical
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
v1 <- serializer::marshall_raw(dat)
v2 <- base::serialize(dat, NULL, xdr = FALSE)
identical(v1, v2)
#> [1] TRUE
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The serialized length should match the calculation from earlier
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
length(v1)
#> [1] 674
head(v1, 200)
#> [1] 42 0a 03 00 00 00 03 03 04 00 00 05 03 00 05 00 00 00 55 54 46 2d 38 13 03
#> [26] 00 00 0b 00 00 00 0e 00 00 00 03 00 00 00 00 00 00 00 00 00 35 40 00 00 00
#> [51] 00 00 00 35 40 cd cc cc cc cc cc 36 40 0e 00 00 00 03 00 00 00 00 00 00 00
#> [76] 00 00 18 40 00 00 00 00 00 00 18 40 00 00 00 00 00 00 10 40 0e 00 00 00 03
#> [101] 00 00 00 00 00 00 00 00 00 64 40 00 00 00 00 00 00 64 40 00 00 00 00 00 00
#> [126] 5b 40 0e 00 00 00 03 00 00 00 00 00 00 00 00 80 5b 40 00 00 00 00 00 80 5b
#> [151] 40 00 00 00 00 00 40 57 40 0e 00 00 00 03 00 00 00 33 33 33 33 33 33 0f 40
#> [176] 33 33 33 33 33 33 0f 40 cd cc cc cc cc cc 0e 40 0e 00 00 00 03 00 00 00 f6
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Unmarshall the raw bytes back into an object
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
serializer::unmarshall_raw(v1)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
#> Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
#> Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
Currently (R 4.3.x) there is no way to handle connections totally within C, as the R API does not expose the functionality.
Instead, to work with connections the following steps need to be taken:
gzfile()
readBin() and writeBin()
tmp <- tempfile()
serializer::marshall_con(head(mtcars, 3), gzfile(tmp))
serializer::unmarshall_con(gzfile(tmp))
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4
#> Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4
#> Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1
calc_serialized_size() can be used to calculate the size of a
serialized object, but does not actually try and create the serialized
object.
Because this does not do any memory allocation, or copying of bytes, the
speed of calc_serialized_size() should give an approximation of the
maximum throughput of the serialization process when using R’s internal
serialization mechanism.
The speeds below seem ridiculously fast! This is because serialization is just passing pointers + lengths to an output stream, and in this special case doing very very little actual memory allocation or copying.
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Test objects
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
N <- 1e7
obj1 <- data.frame(x = sample(N), y = runif(N))
obj2 <- sample(N)
obj3 <- do.call(rbind, replicate(1000, iris, simplify = FALSE))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calc sizes of test objects
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(n1 <- lobstr::obj_size(obj1))
#> 120.00 MB
(n2 <- lobstr::obj_size(obj2))
#> 40.00 MB
(n3 <- lobstr::obj_size(obj3))
#> 5.40 MB
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# go through seritalization process, but only count the bytes
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
res <- bench::mark(
calc_serialized_size(obj1),
calc_serialized_size(obj2),
calc_serialized_size(obj3),
check = FALSE
)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# calc theoretical upper limit
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
res %>%
mutate(MB = as.numeric(c(n1, n2, n3))/1000^2) %>%
mutate(`GB/s` = round(MB/1000 / as.numeric(median), 1)) %>%
mutate(`itr/sec` = round(`itr/sec`)) %>%
mutate(MB = round(MB, 1)) %>%
select(expression, median, `itr/sec`, MB, `GB/s`) %>%
knitr::kable(caption = "Maximum possible throughput of serialization")
| expression | median | itr/sec | MB | GB/s |
|---|---|---|---|---|
| calc_serialized_size(obj1) | 9.91µs | 108476 | 120.0 | 12111.5 |
| calc_serialized_size(obj2) | 3.62µs | 254723 | 40.0 | 11058.5 |
| calc_serialized_size(obj3) | 2.37µs | 347898 | 5.4 | 2284.0 |
Maximum possible throughput of serialization