запись объекта на диск в R через C++ vs. fst

Я был вдохновлен пакетом fst, чтобы попытаться написать функцию C++, чтобы быстро сериализовать некоторые структуры данных, которые у меня есть на R на диск.

Но у меня возникают проблемы с достижением одной и той же скорости записи даже на очень простых объектах. Нижеприведенный код - простой пример написания большого 1 ГБ вектора на диск.

Используя специальный код C++, я достигаю скорость записи 135 МБ/с, что является пределом моего диска в соответствии с CrystalBench.

По тем же данным write_fst достигает скорости записи 223 МБ/с, что кажется невозможным, так как мой диск не может писать так быстро. (Обратите внимание: я использую fst::threads_fst(1) и compress=0, а файлы имеют одинаковый размер данных.)

Что мне не хватает?

Как я могу заставить функцию C++ быстрее записывать на диск?

C++ Код:

#include <Rcpp.h>
#include <fstream>
#include <cstring>
#include <iostream>

// [[Rcpp::plugins(cpp11)]]

using namespace Rcpp;

// [[Rcpp::export]]
void test(SEXP x) {
  char* d = reinterpret_cast<char*>(REAL(x));
  long dl = Rf_xlength(x) * 8;
  std::ofstream OutFile;
  OutFile.open("/tmp/test.raw", std::ios::out | std::ios::binary);
  OutFile.write(d, dl);
  OutFile.close();
}

R Код:

library(microbenchmark)
library(Rcpp)
library(dplyr)
library(fst)
fst::threads_fst(1)

sourceCpp("test.cpp")

x <- runif(134217728) # 1 gigabyte
df <- data.frame(x)

microbenchmark(test(x), write_fst(df, "/tmp/test.fst", compress=0), times=3)
Unit: seconds
                                         expr      min       lq     mean   median       uq      max neval
                                      test(x) 6.549581 7.262408 7.559021 7.975235 8.063740 8.152246     3
 write_fst(df, "/tmp/test.fst", compress = 0) 4.548579 4.570346 4.592398 4.592114 4.614307 4.636501     3

file.info("/tmp/test.fst")$size/1e6
# [1] 1073.742

file.info("/tmp/test.raw")$size/1e6
# [1] 1073.742

Ответы

Ответ 1

Бенчмаркинг. Производительность записи и чтения SSD - это сложный бизнес и трудно сделать правильно. Есть много эффектов, которые следует учитывать.

Например, многие методы SSD используют для ускорения скорости передачи данных (интеллектуально), таких как кэширование DRAM. Эти методы могут увеличить скорость записи, особенно в тех случаях, когда идентичный набор данных записывается на диск несколько раз, как в вашем примере. Чтобы избежать этого эффекта, каждая итерация эталонного теста должна записывать уникальный диск на диск.

Также важны размеры блоков операций записи и чтения: размер физического сектора по умолчанию для SSD составляет 4 КБ. Написание небольших блоков затрудняет работу, но с fst я обнаружил, что запись блоков данных размером более нескольких мегабайт также снижает производительность из - за эффектов кэша процессора. Поскольку fst записывает данные на диск в относительно небольших фрагментах, он обычно быстрее, чем альтернативы, которые записывают данные в один большой блок.

Чтобы облегчить эту блочную запись на SSD, вы можете изменить свой код:

Rcpp::cppFunction('

  #include <fstream>
  #include <cstring>
  #include <iostream>

  #define BLOCKSIZE 262144 // 2^18 bytes per block

  long test_blocks(SEXP x, Rcpp::String path) {
    char* d = reinterpret_cast<char*>(REAL(x));

    std::ofstream outfile;
    outfile.open(path.get_cstring(), std::ios::out | std::ios::binary);

    long dl = Rf_xlength(x) * 8;
    long nr_of_blocks = dl / BLOCKSIZE;

    for (long block_nr = 0; block_nr < nr_of_blocks; block_nr++) {
      outfile.write(&d[block_nr * BLOCKSIZE], BLOCKSIZE);
    }

    long remaining_bytes = dl % BLOCKSIZE;
    outfile.write(&d[nr_of_blocks * BLOCKSIZE], remaining_bytes);

    outfile.close();

    return dl;
    }
')

Теперь мы можем сравнить методы test, test_blocks и fst::write_fst в одном тесте:

x <- runif(134217728) # 1 gigabyte
df <- data.frame(X = x)

fst::threads_fst(1)  # use fst in single threaded mode

microbenchmark::microbenchmark(
  test(x, "test.bin"),
  test_blocks(x, "test.bin"),
  fst::write_fst(df, "test.fst", compress = 0),
  times = 10)
#> Unit: seconds
#>                                          expr      min       lq     mean
#>                           test(x, "test.bin") 1.473615 1.506019 1.590430
#>                    test_blocks(x, "test.bin") 1.018082 1.062673 1.134956
#>  fst::write_fst(df, "test.fst", compress = 0) 1.127446 1.144039 1.249864
#>    median       uq      max neval
#>  1.600055 1.635883 1.765512    10
#>  1.131631 1.204373 1.264220    10
#>  1.261269 1.327304 1.343248    10

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

Обратите внимание, что разница между fst и вашим первоначальным методом test намного менее выражена в моей системе, снова демонстрируя проблемы с использованием тестов для оптимизации системы.