#TL;DR
使用下面的这段代码可以为标准 C++ 流的输入输出提速。
1 |
|
使用时有几个注意事项:
- 关闭了流同步后,切莫将 C++ 风格的输入输出与 C 风格的输入输出(如
scanf
和printf
)混用,否则会造成意料之外的错误。 - 如果需要清空缓冲区,请使用
std::flush
或者std::endl
。 cout.tie(nullptr)
在一般情况下是非必要的,因为其在初始化时并没有绑定其他流。
#原理
#关闭与标准 C 流之间的同步
根据 C++ Reference 上的描述:
std::ios_base::sync_with_stdio
C++
1
static bool sync_with_stdio( bool sync = true );
设置标准 C++ 流是否与标准 C 流在每次输入/输出操作后同步。
标准 C++ 流为下列者:
std::cin
、std::cout
、std::cerr
、std::clog
、std::wcin
、std::wcout
、std::wcerr
和std::wclog
。标准 C 流为下列者:
stdin
、stdout
和stderr
。对于与 C 流 f 同步的标准流
str
,下列函数对拥有等同的效果:
std::fputc(f, c)
和str.rdbuf()->sputc(c)
std::fgetc(f)
和str.rdbuf()->sbumpc()
std::ungetc(c, f)
和str.rdbuf()->sputbackc(c)
实践中,这表示同步的 C++ 流为无缓冲,而每次 C++ 流上的 I/O 都立即应用到对应 C 流的缓冲区。这使得能自由地混合 C++ 与 C I/O。
另外,同步的 C++ 流保证为线程安全(从多个线程输出的单独字符可能交错,但无数据竞争)。
若关闭同步,则允许 C++ 标准流独立地缓冲其 I/O ,可认为这在某些情况下更快。
所有八个标准 C++ 流默认与其相应的 C 流同步。
若在标准流上已出现 I/O 后调用此函数,则行为是实现定义的:有的实现无效果,有的实现销毁读取缓冲区。
可以得知以下信息:
- C++ 为了保证兼容性,默认将标准 C++ 流的数据与标准 C 流同步,并将缓冲数据放置到标准 C 流的缓冲区中,但这样会减慢速度。
- 当关闭同步时会导致程序的不同线程之间的 I/O 冲突,不过 OI 中并不涉及。
- 在程序运行到产生首次输入输出后关闭同步的行为是由实现定义的。
那么可以通过关闭流同步的方式来解决第一点问题:
1 |
|
将这行代码放到主函数的起始位置即可。
由第三点信息可知,不能在运行时反复开启/关闭流同步,防止出现错误。
#使用 \n
代替 std::endl
根据 C++ Reference 上的描述:
std::endl
C++
1
2
template< class CharT, class Traits >
std::basic_ostream<CharT, Traits>& endl( std::basic_ostream<CharT, Traits>& os );插入换行符到输出序列
os
并冲入它,如同调用os.put(os.widen('\n'))
后随os.flush()
。这是仅输出的 I/O 操纵符,可对任何
std::basic_ostream
类型的out
以表达式out << std::endl
调用它。
可以得知,在调用 std::endl
时不仅输出了换行符,而且还清空了缓冲区。众所周知,频繁清空缓冲区会导致程序运行速度的下降,所以应该尽量避免使用 std::endl
作为换行符。
那么可以通过使用 \n
来替代 std::endl
的做法来提升速度:
1 |
|
不推荐使用 endl
的原因是,如果遇到特殊情况需要使用 std::endl
时,define 会将 std::endl
中的 endl
替换为 \n
导致编译错误。
#取消 cin 与 cout 之间的联系
根据 C++ Reference 上的描述:
std::basic_ios
:: tieC++
1
2
std::basic_ostream<CharT,Traits>* tie() const; // (1)
std::basic_ostream<CharT,Traits>* tie( std::basic_ostream<CharT,Traits>* str ); // (2)管理联系流。联系流是输出流,它与流缓冲(
rdbuf()
)所控制的输出序列同步,即在任何*this
上的输入/输出操作前,在联系流上调用flush()
。
- 返回当前联系流。若无联系流,则返回空指针。
- 设置当前联系流为 str 。返回操作前的联系流。若无联系流,则返回空指针。
可以得知,如果一个流设置了联系流,在它进行输入/输出操作前会清除其联系流的缓冲区。众所周知,频繁清空缓冲区会导致程序运行速度的下降,所以如非必要,在竞赛中可以取消 std::cin
和 std::cout
间的联系以获得更快的输入输出速度:
1 |
|
#无需使用 cout.tie(nullptr)
查看 ISO C++ 14 标准(ISO/IEC 14882:2014(E),原文见文末附件):
此处(第 999 页)并没有对 std::cout
的 tie()
赋初值。
在第 1012 页中,指明了 tie()
的初值为 0,即未绑定任何流。
所以,std::cout
在初始时并未与任何流产生绑定,所以无需取消 std::cout
与其他流的绑定。cout.tie(nullptr)
是多余的,可以删去。
#测试
输入数据为 LibreOJ 7. Input Test 的测试点 5,大小为 59.7 MB。测试选择在 LibreOJ 评测量较为平稳时进行。不用洛谷测试的原因是怕被管理封号,我自己的 LibreOJ 账号有管理权限不怕被封。
#结果
用时 | O2 优化 | cin.tie(nullptr) | 与标准 C 流同步 | 换行符 |
---|---|---|---|---|
1820 ms | 是 | 是 | 是 | std::endl |
1041 ms | \n | |||
1107 ms | 否 | std::endl | ||
335 ms | \n | |||
1803 ms | 是 | std::endl | ||
1053 ms | \n | |||
1100 ms | 否 | std::endl | ||
335 ms | \n | |||
1877 ms | 否 | 是 | std::endl | |
1801 ms | \n | |||
1117 ms | 否 | std::endl | ||
1160 ms | \n | |||
1810 ms | 是 | std::endl | ||
1826 ms | \n | |||
1093 ms | 否 | std::endl | ||
1159 ms | \n | |||
1810 ms | 否 | 是 | 是 | std::endl |
1020 ms | \n | |||
1095 ms | 否 | std::endl | ||
347 ms | \n | |||
1808 ms | 是 | std::endl | ||
1045 ms | \n | |||
1105 ms | 否 | std::endl | ||
355 ms | \n | |||
1837 ms | 否 | 是 | std::endl | |
1832 ms | \n | |||
1095 ms | 否 | std::endl | ||
1133 ms | \n | |||
1872 ms | 是 | std::endl | ||
1845 ms | \n | |||
1098 ms | 否 | std::endl | ||
1130 ms | \n |
#代码
1 |
|
#实际测试
#7. Input Test
#10145. 「一本通 4.6 练习 2」郁闷的出纳员
#结语
感谢你完整阅读了本文。
通过本文,相信读者对 C++ 的输入/输出流又有了更深的认识。按照本文所叙述的方法去操作,你就可以获得一个媲美 scanf
和 printf
的速度的 C++ 风格的输入了。
如果本文中有笔者遗漏、编写错误的地方,欢迎指正。
#参考资料
- std::basic_ios<CharT,Traits>::tie,C++ Reference,2021 年 1 月 1 日。
- std::ios_base::sync_with_stdio,2017 年 11 月 23 日。
- std::flush,2017 年 11 月 18 日。
- 27.4 Standard iostream objects,ISO/IEC 14882:2014(E)。
感谢 LibreOJ 提供的高效、稳定的测评服务。