换行并刷新缓冲区

36
在《C++ primer》书的第1章中提到了以下内容:

endl是一个特殊的值,称为操作符,当写入输出流时,它具有将换行符写入输出并刷新与该设备相关联的缓冲区的作用。通过刷新缓冲区,我们确保用户立即看到写入流的输出。

这里的“刷新缓冲区”指的是将缓冲区中的数据真正地写入到输出设备中。在输出一定量的数据后,这些数据会先被存放在缓冲区中而不是直接写入输出设备中,这是为了提高数据的传输效率。但是,如果缓冲区中存放的数据还没有达到一定量就被强制刷新或者程序执行完毕时,会导致输出设备上的数据不全。使用endl操作符可以强制刷新缓冲区,将数据实际地写入到输出设备中,确保输出的完整性和及时性。

请查看:https://dev59.com/C2DVa4cB1Zd3GeqPYwHu#9125587 - Rohit Vipin Mathews
5个回答

46

在写入目标设备之前,输出通常会被缓冲起来。这样,在向访问速度较慢的设备(如文件)进行写入时,不必在每个字符后都访问设备。

刷新意味着清空缓冲区并将其实际写入设备。


1
std::endl 调用 flush() 吗? - John

23

C++的iostreams是有缓冲区的,这意味着当你向ostream输出数据时,内容不会立刻发送到流后面的位置,例如在cout中情况下就不会立即发送到stdout。流的实现决定了何时实际将缓冲区的部分内容发送出去。这样做是为了提高效率,逐字节地写入网络或磁盘流将非常低效,通过使用缓冲区可以解决此问题。

然而,这也意味着当你将调试信息写入日志文件并且程序崩溃时,你可能会丢失一部分已经通过流写入日志文件的数据,因为日志的一部分仍然留在流的缓冲区中尚未写入实际文件。为防止此类情况发生,你需要使流清空其缓冲区,要么通过显式调用flush方法,要么使用endl的方便操作。

然而,如果你只是定期写入文件,你应该使用\n而不是endl,以防止流在每一行结束时不必要地刷新流,从而降低性能。

编辑以包含此注释:

cin和cout有一个特殊的关系,即从cin读取会自动刷新cout。这确保了在等待输入时,用户将首先看到你写入cout的提示。因此,即使在使用cout时,通常也不需要endl,而是可以使用\n。你还可以将其他流绑定在一起以创建这样的关系。


1
此外,在许多Unix系统中,如果标准输出流指向终端,则其为行缓冲,这意味着\n会像endl一样刷新缓冲区。(如果标准输出不是终端,则会采用正常缓冲,您可以通过将输出先通过一些程序进行管道处理(例如./myProg | grep a | less)来查看。) - Daniel Gallagher
2
@Daniel 当然,重要的部分是“如果它进入终端”,你当然不应该假设cout会进入终端,因为它可能会去其他地方。我的观点是,在向cout写入时没有使用endl的好理由。但是,是的,非常好的观点,我完全忘记了。 - wich
@Daniel Gallagher 真是太神奇了!即使输出到终端,./myProg | grep a | less 也不是行缓冲的吗? - John
1
@John 我的程序和grep的标准输出不会被行缓冲,因为它们进入了一个管道。而less的标准输出(几乎可以肯定)会被行缓冲,因为它要输出到终端。 - wich

12
这里的“刷新缓冲区”是什么意思? std::endl 会导致流内部暂存内存(即“缓冲区”)中的数据被“刷新”(转移)到操作系统。随后的行为取决于流映射到的设备类型,但通常来说,刷新将使数据看起来已经被物理传输到了关联的设备上。然而,突然断电可能会破坏这个幻想。
这个刷新会涉及一些开销(浪费时间),因此应该在执行速度很重要的情况下将其最小化。最小化这个开销的整体影响是数据缓冲的基本目的,但过度的刷新会破坏这个目标。

背景信息

计算系统的I/O通常非常复杂,由多个抽象层组成。每个层次可能会引入一定量的开销。数据缓冲是通过最小化两个系统层之间执行的单个事务数量来减少此开销的一种方法。

  • CPU/memory system-level buffering (caching): 对于非常高的活动量,即使是计算机的随机存取存储器系统也可能成为瓶颈。为了解决这个问题,CPU 通过提供多层隐藏缓存(其中单个缓冲区称为缓存行)来虚拟化内存访问。这些处理器缓存缓存您的算法的内存写入(根据 写入策略),以便最小化在内存总线上的冗余访问。

  • 应用级缓冲:虽然这并不总是必要的,但常见的情况是应用程序分配一些内存块来累积输出数据,然后再将其传递给 I/O 库。这提供了允许随机访问的基本好处(如果需要),但这样做的一个重要原因是它最小化了与调用库相关的开销--这可能比简单地写入内存数组要耗费更多时间。

  • I/O 库缓冲C++ IO 流库 可选地为每个打开的流管理一个缓冲区。这个缓冲区用于限制 系统调用 到操作系统内核的数量,因为这些调用往往有一些非常重要的开销。 这是在使用 std::endl 时刷新的缓冲区。

  • 操作系统内核和设备驱动程序:操作系统根据流附加到哪个输出设备将数据路由到特定的设备驱动程序(或子系统)。此时,实际行为可能因该类型设备的性质和特征而大不相同。例如,当设备是硬盘时,设备驱动程序可能不会立即启动传输到设备,而是保持自己的缓冲区,以进一步最小化冗余操作(因为磁盘也最有效地按块写入)。为了显式刷新内核级缓存,可能需要调用诸如 Linux 上的 fsync() 等系统级函数--即使 关闭 相关联的流,也不能强制执行这样的刷新。

    示例输出设备可能包括...

    • 本地计算机上的终端
    • 通过 SSH 或类似方式连接到远程计算机上的终端
    • 通过 管道或套接字 发送到另一个应用程序的数据
    • 许多变体的大容量存储设备和相关文件系统,可能是(再次)本地附加或通过网络分布
  • 硬件缓冲区:特定的硬件可能包含自己的内存缓冲区。例如,硬盘通常包含一个 磁盘缓冲区,以便(除其他事项外)允许物理写入发生而无需使系统的 CPU 参与整个过程。

在许多情况下,这些不同的缓冲层往往是多余的(在某种程度上),因此本质上是过度设计。然而,如果其他层由于与每个层关联的开销而未能提供最佳缓冲,则每个层的缓冲可以提供巨大的吞吐量增益。
简而言之,std::endl仅针对由C++ IO流库管理的特定流的缓冲区。调用std::endl后,数据将已经移动到内核级别管理,并且数据接下来的处理取决于许多因素。

如何避免使用std::endl带来的额外开销


inline std::ostream & endl( std::ostream & os )
   {
   os.put( os.widen('\n') ); // http://en.cppreference.com/w/cpp/io/manip/endl
   if ( debug_mode ) os.flush(); // supply 'debug_mode' however you want
   return os;
   }

在这个例子中,你提供了一个自定义的 endl,可以在调用内部的 flush()(这会强制将内容传输到操作系统)时使用或不使用。启用刷新(使用 debug_mode 变量)对于调试场景非常有用,例如当程序已经终止但相关流尚未被清理(这将强制缓冲区进行最后一次刷新)时,你仍然可以检查输出(例如磁盘文件)。

似乎有必要为涉及到的一些常见概念(IO层,开销和缓冲区)提供一些背景信息,然后描述std::endl在什么方面不做(但某些人可能会天真地认为它会做的事情)--特别是指出刷新到设备驱动程序std::endl会这样做)和刷新到物理设备(它可能不会这样做)之间可能微妙的区别。 - Brent Bradburn
由于断言触发可能是IO缓冲区在程序终止前未被刷新的主要原因,将debug_mode与标准预处理器定义NDEBUG(也控制断言)的存在联系起来是有意义的 - 或者直接使用#ifndef NDEBUG禁用刷新。 - Brent Bradburn

1
使用std::cout时,输出运算符(<<)后使用的操作数被存储在缓冲区中,并且不会显示在stdin(通常是终端或命令提示符)上,直到遇到std::endlstd::cin。这将导致缓冲区被刷新,也就是将缓冲区的内容显示/输出到stdin中。
考虑以下程序:
#include <iostream>
#include <unistd.h>

int main(void)
{
    std::cout << "Hello, world";
    sleep(2);
    std::cout << std::endl;

    return 0;
}

获得的输出将是:
在2秒后
你好,世界

0

一个简单的代码,展示C++中缓冲I/O的效果。

无论您提供什么输入,都会被缓冲,然后在输入时传递给程序变量。

请看下面的代码:

//program to test how buffered  I/O can have unintended effects on our program

#include<bits/stdc++.h>
using namespace std;

int main()
{
    int a;
    char c;
    cin>>a;
    cin>>c;
    cout<<"the number is : "<<a;
    cout<<"\nthe character is : "<<c;
}

在这里,我们声明了两个变量,一个是int类型,另一个是char类型。 如果我们输入的数字为“12d34”, 这将导致int变量仅接受12作为值,并且它将丢弃其余部分,但这些部分仍将存在于缓冲区中。 在下一次输入中,char变量将自动接受值“d”, 而无需询问您是否需要输入任何内容。


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