通过 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;
}
')
现在我们可以在单个基准测试中比较方法 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
在存储列和 table 信息、(可能的)属性、散列和压缩信息方面有一些开销。
请注意,fst
和您最初的 test
方法之间的差异在我的系统上不太明显,再次显示使用基准优化系统的挑战。
我受到 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;
}
')
现在我们可以在单个基准测试中比较方法 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
在存储列和 table 信息、(可能的)属性、散列和压缩信息方面有一些开销。
请注意,fst
和您最初的 test
方法之间的差异在我的系统上不太明显,再次显示使用基准优化系统的挑战。