std::fstream缓冲区与手动缓冲区(为什么手动缓冲区可以获得10倍的性能提升)?

79

我测试了两种写入配置:

  1. Fstream 缓冲:

    // Initialization
    const unsigned int length = 8192;
    char buffer[length];
    std::ofstream stream;
    stream.rdbuf()->pubsetbuf(buffer, length);
    stream.open("test.dat", std::ios::binary | std::ios::trunc)
    
    // To write I use :
    stream.write(reinterpret_cast<char*>(&x), sizeof(x));
    
  2. 手动缓冲:

    // Initialization
    const unsigned int length = 8192;
    char buffer[length];
    std::ofstream stream("test.dat", std::ios::binary | std::ios::trunc);
    
    // Then I put manually the data in the buffer
    
    // To write I use :
    stream.write(buffer, length);
    

我原本期望会得到相同的结果...

但是我的手动缓冲区写入方式使得100MB文件的写入性能提高了10倍,而使用fstream缓冲和正常情况下(不重新定义缓冲区)相比没有任何改变。

有人对这种情况有解释吗?

编辑: 以下是在超级计算机上进行的基准测试(Linux 64位架构,最新英特尔Xeon 8核处理器,Lustre文件系统和...希望配置良好的编译器) benchmark (我没有解释1kB手动缓冲区产生“共振”的原因...)

编辑2: 在1024B处出现了共振现象(如果有人有想法,我很感兴趣): enter image description here


1
你能否检查默认缓冲区大小,就像这个问题中所述:http://stackoverflow.com/questions/10350759/default-buffer-size-of-basic-filebuf-within-libstdc - PiotrNycz
2
我建议您检查C++库的源代码,以查看它根据缓冲区有何不同。 - Some programmer dude
1
@Vincent 好的,那么也许您可以发布与您发布的结果相对应的版本。 - Christian Rau
5
你能否发布你正在进行基准测试的完整可编译测试程序(即使在像ideone.com这样的外部网站上)? - Michael Burr
@MichaelBurr:好的,我会尽快安排时间完成这个任务,因为目前这个基准测试依赖于我正在编写的库中的一些类。 - Vincent
显示剩余9条评论
3个回答

40

这主要是由于函数调用的开销和间接性。ofstream::write() 方法是从 ostream 继承而来。在 libstdc++ 中,该函数没有被内联处理,这是开销的第一个来源。然后,ostream::write() 必须调用 rdbuf()->sputn() 来进行实际的写入操作,这需要进行一次虚函数调用。

除此之外,libstdc++ 将 sputn() 重定向到另一个虚函数 xsputn(),这增加了另一次虚函数调用。

如果您自己将字符放入缓冲区中,则可以避免这种开销。


2
我必须承认我不明白为什么这个答案是正确的。两个版本都使用了stream::write,那么为什么会有所不同呢?pubsetbuf然后打开应该与ifstream cstr具有相同的效果。 - AJed
1
@AJed:问题中没有明确说明,但假设在第一种情况下,当写入“x”时,这些写入是小的,因此您以这种方式进行了更多对于相同数量的数据而言调用stream.write - Vaughn Cato

16

我想解释一下导致第二张图表峰值的原因。

实际上,std::ofstream 使用的虚函数会导致性能下降,类似于我们在第一张图片中看到的情况,但这并不能回答为什么手动缓冲区大小小于1024字节时性能受到最严重的影响。

问题与writev()write()系统调用的高成本以及std::ofstream的内部类std::filebuf的内部实现有关。

为了展示write()如何影响性能,我在Linux机器上使用dd工具进行了简单的测试,复制了10MB文件并更改了不同的缓冲区大小(使用bs选项):

test@test$ time dd if=/dev/zero of=zero bs=256 count=40000
40000+0 records in
40000+0 records out
10240000 bytes (10 MB) copied, 2.36589 s, 4.3 MB/s

real    0m2.370s
user    0m0.000s
sys     0m0.952s
test$test: time dd if=/dev/zero of=zero bs=512 count=20000
20000+0 records in
20000+0 records out
10240000 bytes (10 MB) copied, 1.31708 s, 7.8 MB/s

real    0m1.324s
user    0m0.000s
sys     0m0.476s

test@test: time dd if=/dev/zero of=zero bs=1024 count=10000
10000+0 records in
10000+0 records out
10240000 bytes (10 MB) copied, 0.792634 s, 12.9 MB/s

real    0m0.798s
user    0m0.008s
sys     0m0.236s

test@test: time dd if=/dev/zero of=zero bs=4096 count=2500
2500+0 records in
2500+0 records out
10240000 bytes (10 MB) copied, 0.274074 s, 37.4 MB/s

real    0m0.293s
user    0m0.000s
sys     0m0.064s

正如你所看到的:缓冲区越小,写入速度就越慢,因此dd在系统空间中停留的时间就会越长。因此,当缓冲区大小减小时,读/写速度也会降低。

但是,在话题创建者手动缓冲区测试中,为什么速度在手动缓冲区大小小于1024字节时达到峰值?为什么它几乎保持不变

解释与std::ofstream实现有关,特别是与std::basic_filebuf有关。

默认情况下,它使用1024字节的缓冲区(BUFSIZ变量)。因此,当您使用小于1024的数据段写入数据时,ofstream :: write()操作至少调用2次writev()(第一次写入缓冲区,第二次强制写入第一个和第二个)。根据这一点,我们可以得出结论:ofstream :: write()速度不取决于峰值之前的手动缓冲区大小(很少调用write()两次)。

当您尝试使用ofstream :: write()调用一次写入大于或等于1024字节的缓冲区时,每个ofstream :: write()都会调用writev()系统调用。因此,当手动缓冲区大于1024(峰值之后)时,速度会增加。

此外,如果您想使用streambuf :: pubsetbuf()设置大于1024缓冲区(例如8192字节缓冲区)并使用大小为1024的数据段调用ostream :: write()来写入数据,则会惊讶地发现写入速度与使用1024缓冲区相同。这是因为std::basic_filebuf的实现-std::ofstream的内部类-在传递的缓冲区大于或等于1024字节硬编码强制每个ofstream :: write()调用系统writev()调用(请参见basic_filebuf :: xsputn()源代码)。 GCC bugzilla中还存在一个已报告的问题,该问题报告于2014-11-05

因此,可以通过以下两种情况中的任何一种解决此问题:

  • 用自己的类替换std :: filebuf并重新定义std :: ofstream
  • 将缓冲区划分为小于1024的大小,并逐个传递到ofstream :: write()
  • 不要将小数据段传递给ofstream :: write(),以避免在std::ofstream的虚拟函数上降低性能

3
我想要补充现有的回答,这种性能行为(所有来自虚拟方法调用/间接寻址的开销)通常在编写大块数据时不是问题。问题和之前的回答中似乎省略了一点(尽管可能是隐含理解),即原始代码每次只写入少量字节。只是为了澄清其他人:如果您正在编写大块数据(~kB+),则没有理由期望手动缓冲将与使用std::fstream的缓冲具有显着的性能差异。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接