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

标签 c++ r rcpp

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

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

使用自定义 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 缓存。这些技术可以提高您的写入速度,尤其是在将相同的数据集多次写入磁盘的情况下,例如您的示例。为避免这种影响,基准测试的每次迭代都应将唯一的数据集写入磁盘。

写入和读取操作的 block 大小也很重要:SSD 的默认物理扇区大小为 4KB。写入较小的 block 会影响性能,但使用 fst 我发现写入大于几 MB 的数据 block 也会降低性能,因为 CPU 缓存效应。因为 fst 以相对较小的 block 将其数据写入磁盘,所以它通常比在单个大块中写入数据的替代方案更快。

为了方便这种逐 block 写入 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 在存储列和表信息、(可能的)属性、散列和压缩信息方面有一些开销。

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

关于c++ - 通过 C++ 与 fst 在 R 中将对象写入磁盘,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51702620/

相关文章:

r - R中的快速并行二分距离计算

c++ - 重载 Rcpp 类中继承的方法

c++ - 将 std::vector 作为指针引用传递

c++ - 从 std 数组中删除元素

r - 可以创建每天将 SQL 数据库写入 MongoDB 的管道吗?

R y 与 x 的多个散点图

c++ - 如何在 openmp + mpi 的 While 循环中启动多进程

c++ - SetWindowPos() 跨进程 DPI 感知

r - 将图例中的 ggplot 彩色线更改为正方形或圆形

r - 在 Rcpp 中按列对数据框排序