Skip to content

C++ 输入输出的速度优化与实际测试

杂项约 1.8 千字
检测到 KaTeX 加载失败,可能会导致文中的数学公式无法正常渲染。

#TL;DR

使用下面的这段代码可以为标准 C++ 流的输入输出提速。

C++
1
2
3
const char endl = '\n';
std::ios::sync_with_stdio(false);
cin.tie(nullptr);

使用时有几个注意事项:

  1. 关闭了流同步后,切莫将 C++ 风格的输入输出与 C 风格的输入输出(如 scanfprintf)混用,否则会造成意料之外的错误。
  2. 如果需要清空缓冲区,请使用 std::flush 或者 std::endl
  3. 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::cinstd::coutstd::cerrstd::clogstd::wcinstd::wcoutstd::wcerrstd::wclog

标准 C 流为下列者:stdinstdoutstderr

对于与 C 流 f 同步的标准流 str,下列函数对拥有等同的效果:

  1. std::fputc(f, c)str.rdbuf()->sputc(c)
  2. std::fgetc(f)str.rdbuf()->sbumpc()
  3. 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 后调用此函数,则行为是实现定义的:有的实现无效果,有的实现销毁读取缓冲区。

可以得知以下信息:

  1. C++ 为了保证兼容性,默认将标准 C++ 流的数据与标准 C 流同步,并将缓冲数据放置到标准 C 流的缓冲区中,但这样会减慢速度。
  2. 当关闭同步时会导致程序的不同线程之间的 I/O 冲突,不过 OI 中并不涉及。
  3. 在程序运行到产生首次输入输出后关闭同步的行为是由实现定义的。

那么可以通过关闭流同步的方式来解决第一点问题:

C++
1
std::ios::sync_with_stdio(false);

将这行代码放到主函数的起始位置即可。

由第三点信息可知,不能在运行时反复开启/关闭流同步,防止出现错误。

#使用 \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 的做法来提升速度:

C++
1
2
3
const char endl = '\n';
// 或者(不推荐)
#define endl '\n'

不推荐使用 endl 的原因是,如果遇到特殊情况需要使用 std::endl 时,define 会将 std::endl 中的 endl 替换为 \n 导致编译错误。

#取消 cin 与 cout 之间的联系

根据 C++ Reference 上的描述:

std::basic_ios::tie

C++
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()

  1. 返回当前联系流。若无联系流,则返回空指针。
  2. 设置当前联系流为 str 。返回操作前的联系流。若无联系流,则返回空指针。

可以得知,如果一个流设置了联系流,在它进行输入/输出操作前会清除其联系流的缓冲区。众所周知,频繁清空缓冲区会导致程序运行速度的下降,所以如非必要,在竞赛中可以取消 std::cinstd::cout 间的联系以获得更快的输入输出速度:

C++
1
cin.tie(nullptr);

#无需使用 cout.tie(nullptr)

查看 ISO C++ 14 标准(ISO/IEC 14882:2014(E),原文见文末附件):

此处(第 999 页)并没有对 std::couttie() 赋初值。

在第 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

#代码

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

using std::cin;
using std::cout;

// [1]: 换行符不同
const char endl = '\n';
// 或
// using std::endl;

unsigned long long x, ans;

int main() {
// [2]: 是否与标准 C 流同步
std::ios::sync_with_stdio(false);

// [3]: 是否取消 cin 与其他流之间的关联
cin.tie(nullptr);

// [4]: 是否取消 cout 与其他流之间的关联
cout.tie(nullptr);

for (int i = 0; i < 3000000; i++) {
cin >> x;
cout << (ans ^= x) << endl;
}

return 0;
}

#实际测试

#7. Input Test

  • #1472987,O2 优化 + 读输优化,559 ms。
  • #1473174,O2 优化,2207 ms。

#10145. 「一本通 4.6 练习 2」郁闷的出纳员

  • #1472998,O2 优化 + 读输优化,253 ms。
  • #1473175,O2 优化,399 ms。

#结语

感谢你完整阅读了本文。

通过本文,相信读者对 C++ 的输入/输出流又有了更深的认识。按照本文所叙述的方法去操作,你就可以获得一个媲美 scanfprintf 的速度的 C++ 风格的输入了。

如果本文中有笔者遗漏、编写错误的地方,欢迎指正。

#参考资料

  1. std::basic_ios<CharT,Traits>::tie,C++ Reference,2021 年 1 月 1 日。
  2. std::ios_base::sync_with_stdio,2017 年 11 月 23 日。
  3. std::flush,2017 年 11 月 18 日。
  4. 27.4 Standard iostream objects,ISO/IEC 14882:2014(E)。

感谢 LibreOJ 提供的高效、稳定的测评服务。