如果不做特殊处理,cin/cout的速度比scanf/printf慢得多,做算法时经常Time Limit Exceeded。 但使用一些技巧,cin/cout的速度可以达到甚至超过scanf/printf

测试程序

产生大量随机数并写入文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include<random>
#include<fstream>
#include<functional>
int main(){
    //从hardware entropy source生成随机种子
    std::random_device rd;
    std::mt19937 gen(rd());
    //均匀生成-10000到10000的int,并将生成随机数的操作封装成函数
    std::uniform_int_distribution<int> dis(-10000,10000);
    auto rand_func=std::bind(dis,gen);
    //建立文件流并写入文件./data
    std::ofstream out;
    out.open("./data",std::ofstream::out);
    if(out)
        //写入10^7个随机数
        for(int n=0;n<static_cast<int>(1e7);++n)
            out<<rand_func()<<" ";
    out.close();
    return 0;
}

使用scanf/printf

1
2
3
4
5
6
7
8
//文件名:testc.cc
#include<cstdio>
int main(){
    int temp;
    while(std::scanf("%d",&temp)!=EOF)
        printf("%d\n",temp);
    return 0;
}
1
2
3
4
$ g++ ./testc.cc && time ./a.out <./data >./out
# real    0m4.821s
# user    0m4.188s
# sys     0m0.578s

使用cin/cout

1
2
3
4
5
6
7
8
//文件名:testcpp.cc
#include<iostream>
int main(){
    int temp;
    while(std::cin>>temp)
        std::cout<<temp<<std::endl;
    return 0;
}
1
2
3
4
$ rm ./out && g++ ./testcpp.cc && time ./a.out <./data >./out
# real    3m23.071s
# user    0m22.781s
# sys     2m44.938s

只是使用cin/cout代替了前面的scanf/printf,但scanf/printf只需数秒,而cin/cout需要数分钟

endl的影响

在[2]中有说<<endl等价于<<"\n"<<flush,即每次使用endl都会刷新缓冲区,IO开销很大。

1
2
3
4
5
6
7
8
//文件名:testcpp_noendl.cc
#include<iostream>
int main(){
    int temp;
    while(std::cin>>temp)
        std::cout<<temp<<"\n";
    return 0;
}
1
2
3
4
$ rm ./out && g++ ./testcpp_noendl.cc && time ./a.out <./data >./out
# real    3m21.666s
# user    0m23.156s
# sys     2m44.828s

该版本使用"\n"代替endl,但运行时间和使用endl是一致的。 如果不写入文件./out而是直接打印到终端,还会发现仍是在不断刷缓冲,并没有等到缓冲区满再输出。

tie的影响

造成上面用"\n"代替endl运行时间仍一致的原因是cin被关联(tie)到cout上(原因如[2]所说,与用户交互的程序需保证让用户输入前,已经打印完了需要打印的内容)。 当读写被关联的流时,关联到的流的缓冲被刷新。即,每次读取cin时,cout都被刷新。因此仍是每个循环刷新一次缓冲。

异步读写

一种解决方案是将cin读到的数据存起来,将cin和cout分离

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//文件名:testcpp_noendl_unsync.cc
#include<iostream>
#include<vector>
int main(){
    std::vector<int> vec;
    int temp;
    while(std::cin>>temp)
        vec.push_back(temp);
    for(auto x:vec)
        std::cout<<x<<"\n";
    return 0;
}
1
2
3
4
$ rm ./out && g++ ./testcpp_noendl_unsync.cc && time ./a.out <./data >./out
# real    0m9.092s
# user    0m7.719s
# sys     0m1.297s

该版本将cin和cout分离到两个循环中,异步读写。 即使输入时增加了向vector添加的开销,输出时增加了从vector中取元素的开销, 速度仍大幅提高,达到秒级。

解除关联

更好的解决方案是将cin和cout解除关联,方法是将cin关联到另一个空的流上,即使用cin.tie(nullptr)

1
2
3
4
5
6
7
8
9
//文件名:testcpp_noendl_notie.cc
#include<iostream>
int main(){
    std::cin.tie(nullptr);
    int temp;
    while(std::cin>>temp)
        std::cout<<temp<<"\n";
    return 0;
}
1
2
3
4
$ rm ./out && g++ ./testcpp_noendl_notie.cc && time ./a.out <./data >./out
# real    0m8.980s
# user    0m8.000s
# sys     0m0.813s

解除关联后,即使同步输入/输出,时间也在秒级,但仍比scanf/printf慢

与C流同步的影响

在[5]和[6]中介绍了同时使用scanf/printf和cin/cout时,是在同时使用C stream和C++ stream两种流。 在[6]中提到:

Sets whether the standard C++ streams are synchronized to the standard C streams after each input/output operation.
The standard C++ streams are the following: std::cin, std::cout, std::cerr, std::clog, std::wcin, std::wcout, std::wcerr and std::wclog
The standard C streams are the following: stdin, stdout and stderr

In practice, this means that the synchronized C++ streams are unbuffered, and each I/O operation on a C++ stream is immediately applied to the corresponding C stream’s buffer. This makes it possible to freely mix C++ and C I/O.

在混合使用scanf/printf和cin/cout时,为了保证同步,即保证[5]中的示例代码能以正确的顺序运行,每个C++ stream都会同步到一个C stream上,保证这个同步需要一些开销。

有时候为保证stream的同步,花一点开销是值得的,例如[6]中介绍的保证线程安全的情形。但不需要同步的时候将std::ios_base::sync_with_stdio设为false可减少这些开销。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//文件名:testcpp_unsyncC.cc
#include<iostream>
int main(){
    std::cin.tie(nullptr);
    std::ios_base::sync_with_stdio(false);
    int temp;
    while(std::cin>>temp)
        std::cout<<temp<<"\n";
    return 0;
}
1
2
3
4
$ rm ./out && g++ ./testcpp_unsyncC.cc && time ./a.out <./data >./out
# real    0m4.090s
# user    0m3.375s
# sys     0m0.656s

解除C stream和C++ stream的同步后,时间进一步缩短,使用cin/cout的时间少于使用scanf/printf

根据[1]中描述,在数据量增大时cin/cout比scanf/printf更快的原因之一是: cin/cout是模板类,可以在编译期确定类型。而scanf/printf是普通函数,只能在运行期确定类型

总结

  • 不需要即时打印时,使用<<"\n"代替<<endl,后者会刷新缓冲,产生IO开销
  • 不需要与用户交互时,使用cin.tie(nullptr)将cin和cout解除关联,避免每次读cin都引起cout刷新
  • 不需要保证流同步时,使用ios_base::sync_with_stdio(false)解除C++ stream和C stream的同步,进一步减少开销

其他材料

[1]中比较全面地说了本文的三个要点。
[2]是C++ primer某一节的笔记,讲了缓冲区刷新、endl、tie。
[3]是tie的手册。
[4]是stackoverflow的一个问题,下面回答提到了sync_with_stdio并给出示例。
[5]是libstdc++的手册,讲了同时使用scanf/printf和cin/cout的同步。
[6]是sync_with_stdio的手册,讲了C++ stream和C stream的同步。

Reference

[1]:C++的輸出入cin/cout和scanf/printf誰比較快?
[2]:我的博客:C++ primer 8.1.3 #管理输出缓冲
[3]:cppreference.com - std::basic_ios<CharT,Traits>::tie
[4]:Using scanf() in C++ programs is faster than using cin?
[5]:GNU libstdc++ manual - Chapter 13. Input and Output - Interacting with C
[6]:cppreference.com - std::ios_base::sync_with_stdio