Более быстрый способ нарезать (сырой) вектор? - PullRequest
2 голосов
/ 16 июня 2019

Задача

Я ищу быстрый (в идеале постоянное время) способ взять большой фрагмент длинного необработанного вектора в R. Например:

obj <- raw(2^32)
obj[seq_len(2^31 - 1)]

Даже с ALTREP база R занимает слишком много времени.

system.time(obj[seq_len(2^31 - 1)])
#>    user  system elapsed 
#>  19.470  38.853 148.288 

Почему?

Потому что я пытаюсь ускорить storr на порядок ускорить drake. Я хочу storr, чтобы быстрее сохранять длинные исходные векторы. writeBin() очень быстрый, но по-прежнему не может обрабатывать векторы длиной более 2 ^ 31 - 1 байт . Поэтому я хочу сохранить данные в управляемых фрагментах как , описанное здесь . Это почти работает, но создание чанков происходит слишком медленно и дублирует слишком много данных в памяти.

Идеи

Давайте создадим функцию

slice_raw <- function(obj, from, to) {
  # ???
}

, что по существу эквивалентно

obj[seq(from, to, by = 1L)]

и который равен O (1) как во времени, так и в памяти. Теоретически все, что нам нужно сделать, это

  1. Передача obj в функцию C.
  2. Создать новый указатель на первый байт obj.
  3. Увеличить новый указатель на начало среза.
  4. Создайте RAWSXP в новом указателе соответствующей длины (менее 2 ^ 31 байт).
  5. Вернуть RAWSXP.

У меня есть опыт работы в C, но я изо всех сил пытаюсь получить полный контроль над внутренними элементами R . Я хотел бы получить доступ к указателям C внутри SEXP s, чтобы я мог сделать арифметику базовых указателей и создать R векторов известной длины из необозначенных указателей C. Ресурсы, которые я нашел на внутренних устройствах R, похоже, не объясняют, как обернуть или развернуть указатели. Нужно ли нам Rcpp для этого?

Следующий грубый набросок показывает, что я пытаюсь сделать.

library(inline)
sig <- c(
  x = "raw",         # Long raw vector with more than 2^31 - 1 bytes.
  start = "integer", # Should probably be R_xlen_t.
  bytes = "integer"  # <= 2^31 - 1. Ideally coercible to R_xlen_t.
)
body <- "
Rbyte* result;           // Just a reference. Want to avoid copying data.
result = RAW(x) + start; // Trying to do ordinary pointer arithmetic.
return asRaw(result);    // Want to return a raw vector of length `bytes`.
"
slice_raw <- cfunction(sig = sig, body = body)

РЕДАКТИРОВАТЬ: еще несколько потенциальных обходных путей

Спасибо Дирку за то, что он заставил меня задуматься над этим. Для достаточно небольших данных мы можем использовать fst для сохранения фрейма данных с одним столбцом, где столбец - это необработанный вектор, который нам действительно нужен. Это использование fst быстрее, чем writeBin()

library(fst)
wrapper <- data.frame(actual_data = raw(2^31 - 1))
system.time(write_fst(wrapper, tempfile()))
#>    user  system elapsed 
#>   0.362   0.019   0.103
system.time(writeBin(wrapper$actual_data, tempfile()))
#>    user  system elapsed 
#>   0.314   1.340   1.689

Создан в 2019-06-16 пакетом Представить (v0.3.0)

К сожалению, сложно создать фреймы данных с 2 ^ 31 или более строк. Один из способов - сначала преобразовать необработанный вектор в матрицу, и мы избегаем обычного целочисленного переполнения, поскольку (2 ^ 31 - 1) ^ 2 байта - это несколько эксабайт.

library(fst)
x <- raw(2^32)
m <- matrix(x, nrow = 2^16, ncol = 2^16)
system.time(write_fst(as.data.frame(m), tempfile()))
#>    user  system elapsed 
#>   8.776   1.459   9.519

Создан в 2019-06-16 пакетом Представить (v0.3.0)

Мы все еще оставляем saveRDS() в пыли, но мы больше не бьем writeBin(). Преобразование из фрейма данных в матрицу происходит медленно, и я не уверен, что оно будет хорошо масштабироваться.

library(fst)
x <- raw(2^30)
m <- matrix(x, nrow = 2^15, ncol = 2^15)
system.time(write_fst(as.data.frame(m), tempfile()))
#>    user  system elapsed 
#>   1.998   0.408   2.409
system.time(writeBin(as.raw(m), tempfile()))
#>    user  system elapsed 
#>   0.329   0.839   1.397

Создан в 2019-06-16 пакетом Представить (v0.3.0)

Если, как предложил Дирк, мы можем использовать R_xlen_t для индексации строк фрейма данных, мы можем избежать преобразования чего-либо.

1 Ответ

1 голос
/ 20 июня 2019

Хотя в настоящее время data.frame с длинными векторными столбцами не очень хорошо поддерживаются, вы все равно можете использовать fst для сериализации длинных необработанных векторов:

# method for writing a raw vector to disk
write_raw <- function(x, path, compress = 50) {

  # create a list and add required attributes
  y <- list(X = x)
  attributes(y) <- c(attributes(y), class = "data.frame")

  # serialize and compress to disk
  fst::write_fst(y, path, compress)
}

# create raw vector of length >2^31
x <- rep(as.raw(0:255), 2^23 + 10)

# write raw vector
write_raw(x, "raw_vector.fst", 100)

При такой схеме нет необходимости разбивать вектор на несколько частей (что, как вы уже указали, значительно замедлит сериализацию). Необработанный вектор может быть перечитан без какого-либо копирования или вырезания:

# method for reading a raw vector from disk
read_raw <- function(path) {

  # read from disk
  z <- fst::read_fst(path)

  z$X
}

z <- read_raw("raw_vector.fst")

fst::hash_fst(x) == fst::hash_fst(z)
#> [1] TRUE TRUE

(обратите внимание, что на данный момент вам нужна первая версия для чтения с поддержкой длинных векторов)

В вашей настройке вы всегда будете сериализовать весь необработанный вектор на диск в целом (как и saveRDS(). Поскольку вам не требуется произвольный доступ к сохраненному вектору, метаданные, хранящиеся в файле fst, немного излишне. Вы также можете протестировать установку, где вы сжимаете необработанный вектор, используя compress_fst(), а затем сохраняете результат, используя saveRDS(raw_vec, compress = FALSE).

Преимущество такой установки состоит в том, что компрессор может использовать для сжатия более крупные фрагменты, увеличивая степень сжатия (эффект может быть значительным). Использование больших кусков также может ускорить сжатие.

С другой стороны, недостатком является то, что вы не сжимаете во время записи на диск, как при write_fst(), так что эффект может замедлить вашу сериализацию. И у вас больше нет случайного доступа, но он вам все равно не нужен.

Если вы реализуете двухэтапный процесс (сначала сжимая данные, а затем сериализуя их), вы сможете учесть разные компрессоры, если пользователь выберет это (например, более медленные компрессоры с очень высокой степенью сжатия для медленных дисков).

...