通过 C++ 与 fst 在 R 中将对象写入磁盘

writing an object to disk in R through C++ vs. fst

我受到 fst 包的启发,尝试编写一个 C++ 函数来快速将我在 R 中的一些数据结构序列化到磁盘。

但即使在非常简单的对象上,我也无法达到相同的写入速度。下面的代码是一个将 1 GB 大向量写入磁盘的简单示例。

使用自定义 C++ 代码,我达到了 135 MB/s 的写入速度,这是根据 CrystalBench 的磁盘限制。

在相同的数据上,write_fst达到了223的写入速度MB/s,这似乎是不可能的,因为我的磁盘不能写那么快。 (注意,我使用 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

对 SSD 写入和读取性能进行基准测试是一项棘手的工作,很难做到正确。有很多影响需要考虑。

例如,许多 SSD 使用技术来(智能地)加快数据速度,例如 DRAM 缓存。这些技术可以提高您的写入速度,尤其是在将相同数据集多次写入磁盘的情况下,如您的示例所示。为避免这种影响,基准测试的每次迭代都应将唯一的数据集写入磁盘。

写入和读取操作的块大小也很重要:SSD 的默认物理扇区大小为 4KB。写入较小的数据块会影响性能,但是 fst 我发现由于 CPU 缓存效应,写入大于几 MB 的数据块也会降低性能。因为 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;
    }
')

现在我们可以在单个基准测试中比较方法 testtest_blocksfst::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 在存储列和 table 信息、(可能的)属性、散列和压缩信息方面有一些开销。

请注意,fst 和您最初的 test 方法之间的差异在我的系统上不太明显,再次显示使用基准优化系统的挑战。