iostream::rdbuf() 对于繁重的写入应用程序来说,最佳大小是多少?

What is optimal size of iostream::rdbuf() for heavy writing application?

提问人:Nick 提问时间:6/10/2023 更新时间:6/11/2023 访问量:85

问:

我有应用程序使用在文件中写入大量数据。ofstream

今天偶然,我发现了几个这样的例子:

const size_t bufsize = 256*1024;
char buf[bufsize];
mystream.rdbuf()->pubsetbuf(buf, bufsize);

原始值是多少?4KB?16KB? 我能找到它的方法吗?

我们在这里可以使用的最优值是什么?256KB?1MB的?如果我们能腾出 1GB 呢?

C++ IOstream

评论

3赞 Pepijn Kramer 6/10/2023
我不确定,但对于 boost::asio,最佳缓冲区大小是与所用设备(磁盘/网络适配器)的 DMA 块大小相匹配的大小。因为这样升压将回退到 DMA 传输(这也降低了 CPU 负载)。我不知道这是否也适用于流。因此,如果您真的有一个 bottlneck,您也可以检查 boost::asio。提示:首先制作一些独立的测试应用,然后尝试各种大小来测量具有不同缓冲区大小(流和 boost::asio)的吞吐速度,可能还有分析器。
0赞 Homer512 6/10/2023
重点可能是情绪。如果块较大,则数据无论如何都不会放入该缓冲区,而是直接写入操作系统页面缓存。如果你的块太小,特别是对于格式化的输出,每次调用的开销会在磁盘出现瓶颈之前出现瓶颈(请记住,每次调用都是互斥锁/解锁)。话虽如此,大约 1 MiB 的缓冲区工作正常。特别是在网络文件系统上,您需要较大的缓冲区。但你肯定想对此进行基准测试stream.write
0赞 Nick 6/10/2023
它主要写入小的二进制块,大多是 100-200 字节,但一个接一个地快速完成。
0赞 Homer512 6/10/2023
@Nick,你总共写了多少数据?
0赞 Nick 6/10/2023
4-8 GB,有时为 30 GB,最低为 200-300 MB,但在极少数情况下

答:

2赞 Homer512 6/11/2023 #1

首先,让我们讨论一下实际情况。我们控制的主要是一些 memcpys。

  1. 我们填充自己的数据结构
  2. ofstream将该结构复制到其内部缓冲区中
  3. 内核将该缓冲区复制到页面缓存中
  4. 磁盘子系统通过 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::writeofstream::writeofstream

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 在您想要支持的所有平台上广泛可用,然后尝试 。ofstreamofstreamosyncstream

评论

0赞 Nick 6/11/2023
多谢。我知道这个问题已经得到解答,但是如果第 2 步很昂贵,切换到旧的 C FILE* 会更快吗?它也是缓冲的。
0赞 Homer512 6/11/2023
@Nick,不,工作原理相同。它有助于格式化的 IO,因为锁不会在单个调用中释放,并且可以比在单个调用中执行更多操作。Glibc 提供解锁的 stdio,但其中大部分是非标准的FILEprintfostream<<
0赞 Nick 6/11/2023
我看到了带有 nullptr 的缓冲区示例,但我认为非缓冲肯定会更慢
0赞 Homer512 6/11/2023
@Nick你的意思是?是的,这通常不是一个好主意。当输入大小大于缓冲区时,将根本不使用其内部缓冲区。但要小心,setvbuf(nullptr, nbytes) 只是让分配缓冲区pubsetbuf(nullptr, 0)ostreamFILE
0赞 Homer512 6/11/2023
@Nick,但不要将直接 IO 与无缓冲 IO 混淆。这些是非常不同的东西