提问人:Nick 提问时间:6/10/2023 更新时间:6/11/2023 访问量:85
iostream::rdbuf() 对于繁重的写入应用程序来说,最佳大小是多少?
What is optimal size of iostream::rdbuf() for heavy writing application?
问:
我有应用程序使用在文件中写入大量数据。ofstream
今天偶然,我发现了几个这样的例子:
const size_t bufsize = 256*1024;
char buf[bufsize];
mystream.rdbuf()->pubsetbuf(buf, bufsize);
原始值是多少?4KB?16KB? 我能找到它的方法吗?
我们在这里可以使用的最优值是什么?256KB?1MB的?如果我们能腾出 1GB 呢?
答:
首先,让我们讨论一下实际情况。我们控制的主要是一些 memcpys。
- 我们填充自己的数据结构
ofstream
将该结构复制到其内部缓冲区中- 内核将该缓冲区复制到页面缓存中
- 磁盘子系统通过 DMA 读取页面缓存
如果数据结构大于缓冲区,则跳过步骤 2。步骤 2 还需要锁定和解斥锁,除非您通过 C++20 的 osyncstream
将其锁定。
根据我的经验,第 2 步可能会产生很大的开销。因此,您可以做的是通过缓冲多个较小的写入请求来人为地增加步骤 1 的大小。下面是一个简单的基准测试来测试这一点:
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <memory>
#include <random>
#include <vector>
int main(int argc, char** argv)
{
if(argc != 5) {
std::cerr << "Usage: " << (argc ? argv[0] : "binary")
<< " filename filesize filebuffer membuffer\n";
return 1;
}
const char* filename = argv[1];
/* Total file size to hit or exceed */
unsigned long long filesize = std::strtoull(argv[2], nullptr, 10);
/*
* Size of the ofstream-internal buffer
* Setting this to 0 uses the platform-default
*/
unsigned long long filebufsize = std::strtoull(argv[3], nullptr, 10);
/*
* Number of bytes to buffer before calling into ofstream
* Setting this to 0 is equivalent to calling ofstream directly
*/
unsigned long long membufsize = std::strtoull(argv[4], nullptr, 10);
auto filebuf = std::make_unique<char[]>(filebufsize);
std::ofstream out(filename);
if(filebufsize > 0)
out.rdbuf()->pubsetbuf(filebuf.get(), filebufsize);
std::default_random_engine rng;
// 100-200 bytes at once
std::uniform_int_distribution<std::size_t> len_distr(100, 200);
std::vector<char> membuf;
for(std::size_t written = 0; written < filesize; written += membuf.size()) {
membuf.clear();
do {
/* Simulates buffering multiple data blocks before calling ofstream */
std::size_t blocksize = len_distr(rng);
membuf.resize(membuf.size() + blocksize);
} while(membuf.size() < membufsize);
out.write(membuf.data(), membuf.size());
}
}
下面是一个 bash 脚本,用于运行默认值和缓冲区大小介于 4 kiB 和 1 GiB 之间的参数组合。
#!/bin/bash
FILE=/dev/null
# 10 GiB
FILESIZE=$((10*1024**3))
run() {
local filebuf="$1"
local membuf="$2"
echo "$filebuf $membuf"
time -p ./a.out "$FILE" "$FILESIZE" "$filebuf" "$membuf"
}
# warmup. ignore first run
run 0 0
run 0 0
for((membuf=4096; membuf<=$((1024**3)); membuf*=2)); do
run 0 $membuf
done
for((filebuf=4096; filebuf<=$((1024**3)); filebuf*=2)); do
run $filebuf 0
for((membuf=4096; membuf<filebuf; membuf*=2)); do
run $filebuf $membuf
done
done
/dev/null 测试
我的假设是,如果缓冲区大小低于 2 级缓存大小,则前 3 个步骤最快,以便最大化内存带宽。我已经在线程撕裂器 CPU 上测试了这一点。前两次测试运行显示以下结果:
0 0
real 3,99
user 3,79
sys 0,20
0 4096
real 1,72
user 1,33
sys 0,38
“0 0”表示我们一次只需调用 100-200 个字节,无需进一步更改。“0 4096”表示我们在调用之前缓冲了大约 4 kiB 的数据。这已经将运行时间缩短了一半!的开销是巨大的。我不会显示所有数据。较大的载体大小在 64 kiB 和 2 MiB 之间显示出相对平坦的性能。在这次特定的运行中,最佳性能是 1.25 秒,容量为 256 kiB。尺寸越大,性能也会如预期的那样下降。ofstream::write
ofstream::write
ofstream
0 131072
real 1,29
user 1,28
sys 0,01
0 262144
real 1,25
user 1,24
sys 0,00
0 524288
real 1,29
user 1,28
sys 0,00
0 1048576
real 1,25
user 1,24
sys 0,00
[...]
0 536870912
real 1,46
user 1,36
sys 0,09
0 1073741824
real 1,57
user 1,40
sys 0,17
将缓冲液大小增加到相似的水平没有积极影响,例如ofstream
262144 0
real 3,81
user 3,64
sys 0,16
262144 4096
real 1,71
user 1,39
sys 0,32
所有其他变化基本上都验证了这些趋势。在不使用更大的内存缓冲区的情况下将缓冲区增加到 1 GiB 时,性能最差ofstream
1073741824 0
real 4,15
user 3,83
sys 0,32
虽然未显示,但 RAM 磁盘上的测试具有相似的性能数字,只是 SYS 负载更高。tmpfs
真实文件系统测试
在第二个测试中,我将文件更改为 NVME SSD 上的 Ext4 文件系统。在这里,更高的开销并不重要,因为它仍然可以胜过 SSD。10 GiB 为 33 秒意味着我们得到大约 310 MiB/s。不过,我们仍然通过预缓冲来节省 CPU 时间。ofstream
0 0
real 33,46
user 3,77
sys 6,49
0 4096
real 33,27
user 1,47
sys 7,32
除此之外,真的没什么可看的,我在完成 200 MiB 块大小之前中止了测试。
其他方面
如果在网络或集群文件系统上运行,性能数据可能看起来非常不同。这些倾向于支持更大的块大小,但这对于读取可能比写入更重要,以减少网络往返次数。多线程还有助于始终保持数据传输的每个组件忙碌。
在速度更快的本地文件系统上,如高性能 U2 SSD 的 RAID 或大型 RAID6 HDD 阵列,我发现普通的页面缓存 IO 不会耗尽磁盘带宽。在这些情况下,我切换到直接 IO,每个块大约 1 MiB 大小,可能重叠 4 个块(异步 IO、线程、Windows 重叠 IO)。但是,如果总写入大小小于主内存,您可能仍希望接受较慢的页面缓存写入性能,以换取将数据保留在缓存中以供读取。
结论
对于您在常规旧桌面系统上可能找到的任何内容,请不要为缓冲区大小而烦恼。在 之外缓冲几百 kiB 或等到 C++20 在您想要支持的所有平台上广泛可用,然后尝试 。ofstream
ofstream
osyncstream
评论
FILE
printf
ostream
<<
setvbuf(nullptr, nbytes)
只是让分配缓冲区pubsetbuf(nullptr, 0)
ostream
FILE
评论
stream.write