为什么C ++中的缓冲很重要?

23

我试图打印 Hello World 200,000 次,但它花了很长时间,所以我不得不停止。但是在我添加一个 char 数组作为缓冲区之后,只需要不到10秒的时间。为什么呢?

添加缓冲区之前:

#include <iostream> 
using namespace std;

int main() {
        int count = 0;
        std::ios_base::sync_with_stdio(false);
        for(int i = 1; i < 200000; i++)
        {       
                cout << "Hello world!\n";
                count++;
        }
                cout<<"Count:%d\n"<<count;
return 0;
}

加入缓冲区后的代码:

#include <iostream> 
using namespace std;

int main() {
        int count = 0;
        std::ios_base::sync_with_stdio(false);
        char buffer[1024];
        cout.rdbuf()->pubsetbuf(buffer, 1024);
        for(int i = 1; i < 200000; i++)
        {       
                cout << "Hello world!\n";
                count++;
        }
                cout<<"Count:%d\n"<<count;
return 0;
}

这让我想到Java。使用BufferReader读取文件有什么优点?


1
基本上,一次写入20个元素比重复写入一个元素20次要快。 - Anycorn
5个回答

31

针对文件操作,直接写入内存(RAM)比直接写入磁盘文件要快。

为了说明,我们定义:

  • 每次向磁盘文件写入操作的成本为1毫秒
  • 每次通过网络向磁盘文件写入操作的成本为5毫秒
  • 每次向内存写入操作的成本为0.5毫秒

假设我们需要将一些数据写入文件100次。

情况1:直接写入磁盘文件

100 times x 1 ms = 100 ms

案例2:直接通过网络将数据写入磁盘文件

100 times x 5 ms = 500 ms

情况三:先将缓冲区加载到内存中,然后再写入磁盘文件

(100 times x 0.5 ms) + 1 ms = 51 ms

情况4:在网络传输前先缓存到内存中再写入磁盘文件

(100 times x 0.5 ms) + 5 ms = 55 ms

结论

在内存中进行缓冲操作总是比直接操作更快。然而,如果您的系统内存不足并且必须与页面文件交换,则速度会再次变慢。因此,您需要平衡内存和磁盘/网络之间的IO操作。


我明白了。所以,直接将数据写入文件比使用缓冲一次性写入要慢得多。但是,难道不是将所有数据一次性写入缓冲区会比分批多次写入文件更耗时吗?我猜一次性写入所有数据的时间要比直接多次写入文件但每次只写入少量数据的时间少一些。 - Amumu
抱歉阿木木,我没有理解你的问题。能否重新表述一下? - mauris
抱歉,这里有点混乱。因此,一次性写入所有数据比每次写入少量数据更好,因为它具有较少复杂的IO调用。 - Amumu
在我的第二个使用缓冲区的例子中,它会填充缓冲区直到满,并将其输出到控制台,然后丢弃缓冲区中的旧数据以接收新数据。这样正确吗? - Amumu
10
这就像用汽车从一个城市搬家到另一个城市。如果你一次只搬一个箱子,那么你会花费很多时间在路上。你希望每次至少搬运一整车的物品。随着每个调用的大小增加,性能和效率一开始会显著提高。然而,最终你会达到收益递减的点。这个点取决于很多因素,但通常在2KB左右。"Hello world!"远小于2KB。 - David Schwartz
显示剩余2条评论

5
写入磁盘的主要问题在于写入所需时间不是字节数的线性函数,而是一个带有巨大常数的仿射函数。
在计算机术语中,这意味着对于IO,您具有良好的吞吐量(小于内存,但仍然相当好),但延迟较差(通常比网络略好)。
如果您查看HDD或SSD的评估文章,您会注意到读/写测试分为两类:
- 随机读取吞吐量 - 连续读取吞吐量
后者通常显著大于前者。
通常,操作系统和IO库应该为您抽象出这一点,但正如您所注意到的,如果您的例程IO密集,增加缓冲区大小可能会使您受益。这是正常的,库通常是为各种用途量身定制的,因此为平均应用程序提供了一个良好的中间地带。如果您的应用程序不是“平均”的,则可能无法以最快的速度运行。

3
你使用的编译器/平台是什么?我在这里看不出任何显著的差异(RedHat,gcc 4.1.2); 两个程序都需要5-6秒才能完成(但“用户”时间约为150毫秒)。如果我将输出重定向到文件(通过shell),总时间约为300毫秒(因此大部分6秒钟的时间用于等待我的控制台赶上程序)。
换句话说,默认情况下应该对输出进行缓冲,所以我很好奇为什么您看到了如此巨大的加速。
与此相关的三个注意事项:
1.你的程序存在一个偏移一的错误,因为你只打印了199999次,而不是规定的200000次(要么从i = 0开始,要么以i<=200000结束) 2.当输出计数时,你混合了printf语法和cout语法......修复这个问题很容易。 3.禁用sync_with_stdio可以在将输出重定向到控制台时略微加快速度(约5%),但在重定向到文件时影响可以忽略。这是一个微观优化,在大多数情况下可能不需要(个人意见)。

我在 Windows XP 上使用 GCC 运行了代码块中的两个代码示例。没有缓冲区的代码执行时间为 27 毫秒,而有缓冲区的代码执行时间为 17 毫秒。 - Aditya P
我在Visual Studio 2010 Professional,Windows Server 2008 R2上运行了它。第一个没有缓冲的示例花费了很长时间,但是有了缓冲就不会了。 - Amumu

2

cout函数包含了许多隐藏的、复杂的逻辑,一直延伸到内核,这样你就可以将文本写入屏幕。当你以这种方式使用缓冲区时,实际上是进行批处理请求,而不是重复复杂的I/O调用。


1
如果使用缓冲区,您将获得更少的实际I/O调用,这是较慢的部分。首先,缓冲区被填充,然后进行一次I/O调用来刷新缓冲区。在Java或任何其他I/O较慢的系统中同样有帮助。

1
标准输出流没有缓冲区? - Basilevs

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